Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
- Academy, Whistleblower, Incidents frontend pages with API proxies and types - Vendor compliance API proxy route - Go backend handlers and models for all new SDK modules - Investor pitch-deck app with interactive slides - Blog section with DSGVO, AI Act, NIS2, glossary articles - MkDocs documentation site - CI/CD pipelines (Woodpecker, GitHub Actions), security scanning config - Planning and implementation documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
851 lines
24 KiB
Go
851 lines
24 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/vendor"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// VendorHandlers handles vendor-compliance HTTP requests
|
|
type VendorHandlers struct {
|
|
store *vendor.Store
|
|
}
|
|
|
|
// NewVendorHandlers creates new vendor handlers
|
|
func NewVendorHandlers(store *vendor.Store) *VendorHandlers {
|
|
return &VendorHandlers{store: store}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Vendor CRUD
|
|
// ============================================================================
|
|
|
|
// CreateVendor creates a new vendor
|
|
// POST /sdk/v1/vendors
|
|
func (h *VendorHandlers) CreateVendor(c *gin.Context) {
|
|
var req vendor.CreateVendorRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
userID := rbac.GetUserID(c)
|
|
|
|
v := &vendor.Vendor{
|
|
TenantID: tenantID,
|
|
Name: req.Name,
|
|
LegalForm: req.LegalForm,
|
|
Country: req.Country,
|
|
Address: req.Address,
|
|
Website: req.Website,
|
|
ContactName: req.ContactName,
|
|
ContactEmail: req.ContactEmail,
|
|
ContactPhone: req.ContactPhone,
|
|
ContactDepartment: req.ContactDepartment,
|
|
Role: req.Role,
|
|
ServiceCategory: req.ServiceCategory,
|
|
ServiceDescription: req.ServiceDescription,
|
|
DataAccessLevel: req.DataAccessLevel,
|
|
ProcessingLocations: req.ProcessingLocations,
|
|
Certifications: req.Certifications,
|
|
ReviewFrequency: req.ReviewFrequency,
|
|
ProcessingActivityIDs: req.ProcessingActivityIDs,
|
|
TemplateID: req.TemplateID,
|
|
Status: vendor.VendorStatusActive,
|
|
CreatedBy: userID.String(),
|
|
}
|
|
|
|
if err := h.store.CreateVendor(c.Request.Context(), v); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"vendor": v})
|
|
}
|
|
|
|
// ListVendors lists all vendors for a tenant
|
|
// GET /sdk/v1/vendors
|
|
func (h *VendorHandlers) ListVendors(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
vendors, err := h.store.ListVendors(c.Request.Context(), tenantID.String())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"vendors": vendors,
|
|
"total": len(vendors),
|
|
})
|
|
}
|
|
|
|
// GetVendor retrieves a vendor by ID with contracts and findings
|
|
// GET /sdk/v1/vendors/:id
|
|
func (h *VendorHandlers) GetVendor(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
id := c.Param("id")
|
|
|
|
v, err := h.store.GetVendor(c.Request.Context(), tenantID.String(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if v == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "vendor not found"})
|
|
return
|
|
}
|
|
|
|
contracts, _ := h.store.ListContracts(c.Request.Context(), tenantID.String(), &id)
|
|
findings, _ := h.store.ListFindings(c.Request.Context(), tenantID.String(), &id, nil)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"vendor": v,
|
|
"contracts": contracts,
|
|
"findings": findings,
|
|
})
|
|
}
|
|
|
|
// UpdateVendor updates a vendor
|
|
// PUT /sdk/v1/vendors/:id
|
|
func (h *VendorHandlers) UpdateVendor(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
id := c.Param("id")
|
|
|
|
v, err := h.store.GetVendor(c.Request.Context(), tenantID.String(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if v == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "vendor not found"})
|
|
return
|
|
}
|
|
|
|
var req vendor.UpdateVendorRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Apply non-nil fields
|
|
if req.Name != nil {
|
|
v.Name = *req.Name
|
|
}
|
|
if req.LegalForm != nil {
|
|
v.LegalForm = *req.LegalForm
|
|
}
|
|
if req.Country != nil {
|
|
v.Country = *req.Country
|
|
}
|
|
if req.Address != nil {
|
|
v.Address = req.Address
|
|
}
|
|
if req.Website != nil {
|
|
v.Website = *req.Website
|
|
}
|
|
if req.ContactName != nil {
|
|
v.ContactName = *req.ContactName
|
|
}
|
|
if req.ContactEmail != nil {
|
|
v.ContactEmail = *req.ContactEmail
|
|
}
|
|
if req.ContactPhone != nil {
|
|
v.ContactPhone = *req.ContactPhone
|
|
}
|
|
if req.ContactDepartment != nil {
|
|
v.ContactDepartment = *req.ContactDepartment
|
|
}
|
|
if req.Role != nil {
|
|
v.Role = *req.Role
|
|
}
|
|
if req.ServiceCategory != nil {
|
|
v.ServiceCategory = *req.ServiceCategory
|
|
}
|
|
if req.ServiceDescription != nil {
|
|
v.ServiceDescription = *req.ServiceDescription
|
|
}
|
|
if req.DataAccessLevel != nil {
|
|
v.DataAccessLevel = *req.DataAccessLevel
|
|
}
|
|
if req.ProcessingLocations != nil {
|
|
v.ProcessingLocations = req.ProcessingLocations
|
|
}
|
|
if req.Certifications != nil {
|
|
v.Certifications = req.Certifications
|
|
}
|
|
if req.InherentRiskScore != nil {
|
|
v.InherentRiskScore = req.InherentRiskScore
|
|
}
|
|
if req.ResidualRiskScore != nil {
|
|
v.ResidualRiskScore = req.ResidualRiskScore
|
|
}
|
|
if req.ManualRiskAdjustment != nil {
|
|
v.ManualRiskAdjustment = req.ManualRiskAdjustment
|
|
}
|
|
if req.ReviewFrequency != nil {
|
|
v.ReviewFrequency = *req.ReviewFrequency
|
|
}
|
|
if req.LastReviewDate != nil {
|
|
v.LastReviewDate = req.LastReviewDate
|
|
}
|
|
if req.NextReviewDate != nil {
|
|
v.NextReviewDate = req.NextReviewDate
|
|
}
|
|
if req.ProcessingActivityIDs != nil {
|
|
v.ProcessingActivityIDs = req.ProcessingActivityIDs
|
|
}
|
|
if req.Status != nil {
|
|
v.Status = *req.Status
|
|
}
|
|
if req.TemplateID != nil {
|
|
v.TemplateID = req.TemplateID
|
|
}
|
|
|
|
if err := h.store.UpdateVendor(c.Request.Context(), v); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"vendor": v})
|
|
}
|
|
|
|
// DeleteVendor deletes a vendor
|
|
// DELETE /sdk/v1/vendors/:id
|
|
func (h *VendorHandlers) DeleteVendor(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
id := c.Param("id")
|
|
|
|
if err := h.store.DeleteVendor(c.Request.Context(), tenantID.String(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "vendor deleted"})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Contract CRUD
|
|
// ============================================================================
|
|
|
|
// CreateContract creates a new contract for a vendor
|
|
// POST /sdk/v1/vendors/contracts
|
|
func (h *VendorHandlers) CreateContract(c *gin.Context) {
|
|
var req vendor.CreateContractRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
userID := rbac.GetUserID(c)
|
|
|
|
contract := &vendor.Contract{
|
|
TenantID: tenantID,
|
|
VendorID: req.VendorID,
|
|
FileName: req.FileName,
|
|
OriginalName: req.OriginalName,
|
|
MimeType: req.MimeType,
|
|
FileSize: req.FileSize,
|
|
StoragePath: req.StoragePath,
|
|
DocumentType: req.DocumentType,
|
|
Parties: req.Parties,
|
|
EffectiveDate: req.EffectiveDate,
|
|
ExpirationDate: req.ExpirationDate,
|
|
AutoRenewal: req.AutoRenewal,
|
|
RenewalNoticePeriod: req.RenewalNoticePeriod,
|
|
Version: req.Version,
|
|
PreviousVersionID: req.PreviousVersionID,
|
|
ReviewStatus: "PENDING",
|
|
CreatedBy: userID.String(),
|
|
}
|
|
|
|
if contract.Version == "" {
|
|
contract.Version = "1.0"
|
|
}
|
|
|
|
if err := h.store.CreateContract(c.Request.Context(), contract); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"contract": contract})
|
|
}
|
|
|
|
// ListContracts lists contracts for a tenant
|
|
// GET /sdk/v1/vendors/contracts
|
|
func (h *VendorHandlers) ListContracts(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
var vendorID *string
|
|
if vid := c.Query("vendor_id"); vid != "" {
|
|
vendorID = &vid
|
|
}
|
|
|
|
contracts, err := h.store.ListContracts(c.Request.Context(), tenantID.String(), vendorID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"contracts": contracts,
|
|
"total": len(contracts),
|
|
})
|
|
}
|
|
|
|
// GetContract retrieves a contract by ID
|
|
// GET /sdk/v1/vendors/contracts/:id
|
|
func (h *VendorHandlers) GetContract(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
id := c.Param("id")
|
|
|
|
contract, err := h.store.GetContract(c.Request.Context(), tenantID.String(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if contract == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "contract not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"contract": contract})
|
|
}
|
|
|
|
// UpdateContract updates a contract
|
|
// PUT /sdk/v1/vendors/contracts/:id
|
|
func (h *VendorHandlers) UpdateContract(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
id := c.Param("id")
|
|
|
|
contract, err := h.store.GetContract(c.Request.Context(), tenantID.String(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if contract == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "contract not found"})
|
|
return
|
|
}
|
|
|
|
var req vendor.UpdateContractRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if req.DocumentType != nil {
|
|
contract.DocumentType = *req.DocumentType
|
|
}
|
|
if req.Parties != nil {
|
|
contract.Parties = req.Parties
|
|
}
|
|
if req.EffectiveDate != nil {
|
|
contract.EffectiveDate = req.EffectiveDate
|
|
}
|
|
if req.ExpirationDate != nil {
|
|
contract.ExpirationDate = req.ExpirationDate
|
|
}
|
|
if req.AutoRenewal != nil {
|
|
contract.AutoRenewal = *req.AutoRenewal
|
|
}
|
|
if req.RenewalNoticePeriod != nil {
|
|
contract.RenewalNoticePeriod = *req.RenewalNoticePeriod
|
|
}
|
|
if req.ReviewStatus != nil {
|
|
contract.ReviewStatus = *req.ReviewStatus
|
|
}
|
|
if req.ReviewCompletedAt != nil {
|
|
contract.ReviewCompletedAt = req.ReviewCompletedAt
|
|
}
|
|
if req.ComplianceScore != nil {
|
|
contract.ComplianceScore = req.ComplianceScore
|
|
}
|
|
if req.Version != nil {
|
|
contract.Version = *req.Version
|
|
}
|
|
if req.ExtractedText != nil {
|
|
contract.ExtractedText = *req.ExtractedText
|
|
}
|
|
if req.PageCount != nil {
|
|
contract.PageCount = req.PageCount
|
|
}
|
|
|
|
if err := h.store.UpdateContract(c.Request.Context(), contract); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"contract": contract})
|
|
}
|
|
|
|
// DeleteContract deletes a contract
|
|
// DELETE /sdk/v1/vendors/contracts/:id
|
|
func (h *VendorHandlers) DeleteContract(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
id := c.Param("id")
|
|
|
|
if err := h.store.DeleteContract(c.Request.Context(), tenantID.String(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "contract deleted"})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Finding CRUD
|
|
// ============================================================================
|
|
|
|
// CreateFinding creates a new compliance finding
|
|
// POST /sdk/v1/vendors/findings
|
|
func (h *VendorHandlers) CreateFinding(c *gin.Context) {
|
|
var req vendor.CreateFindingRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
finding := &vendor.Finding{
|
|
TenantID: tenantID,
|
|
VendorID: req.VendorID,
|
|
ContractID: req.ContractID,
|
|
FindingType: req.FindingType,
|
|
Category: req.Category,
|
|
Severity: req.Severity,
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
Recommendation: req.Recommendation,
|
|
Citations: req.Citations,
|
|
Status: vendor.FindingStatusOpen,
|
|
Assignee: req.Assignee,
|
|
DueDate: req.DueDate,
|
|
}
|
|
|
|
if err := h.store.CreateFinding(c.Request.Context(), finding); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"finding": finding})
|
|
}
|
|
|
|
// ListFindings lists findings for a tenant
|
|
// GET /sdk/v1/vendors/findings
|
|
func (h *VendorHandlers) ListFindings(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
var vendorID, contractID *string
|
|
if vid := c.Query("vendor_id"); vid != "" {
|
|
vendorID = &vid
|
|
}
|
|
if cid := c.Query("contract_id"); cid != "" {
|
|
contractID = &cid
|
|
}
|
|
|
|
findings, err := h.store.ListFindings(c.Request.Context(), tenantID.String(), vendorID, contractID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"findings": findings,
|
|
"total": len(findings),
|
|
})
|
|
}
|
|
|
|
// GetFinding retrieves a finding by ID
|
|
// GET /sdk/v1/vendors/findings/:id
|
|
func (h *VendorHandlers) GetFinding(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
id := c.Param("id")
|
|
|
|
finding, err := h.store.GetFinding(c.Request.Context(), tenantID.String(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if finding == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "finding not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"finding": finding})
|
|
}
|
|
|
|
// UpdateFinding updates a finding
|
|
// PUT /sdk/v1/vendors/findings/:id
|
|
func (h *VendorHandlers) UpdateFinding(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
id := c.Param("id")
|
|
|
|
finding, err := h.store.GetFinding(c.Request.Context(), tenantID.String(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if finding == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "finding not found"})
|
|
return
|
|
}
|
|
|
|
var req vendor.UpdateFindingRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if req.FindingType != nil {
|
|
finding.FindingType = *req.FindingType
|
|
}
|
|
if req.Category != nil {
|
|
finding.Category = *req.Category
|
|
}
|
|
if req.Severity != nil {
|
|
finding.Severity = *req.Severity
|
|
}
|
|
if req.Title != nil {
|
|
finding.Title = *req.Title
|
|
}
|
|
if req.Description != nil {
|
|
finding.Description = *req.Description
|
|
}
|
|
if req.Recommendation != nil {
|
|
finding.Recommendation = *req.Recommendation
|
|
}
|
|
if req.Citations != nil {
|
|
finding.Citations = req.Citations
|
|
}
|
|
if req.Status != nil {
|
|
finding.Status = *req.Status
|
|
}
|
|
if req.Assignee != nil {
|
|
finding.Assignee = *req.Assignee
|
|
}
|
|
if req.DueDate != nil {
|
|
finding.DueDate = req.DueDate
|
|
}
|
|
if req.Resolution != nil {
|
|
finding.Resolution = *req.Resolution
|
|
}
|
|
|
|
if err := h.store.UpdateFinding(c.Request.Context(), finding); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"finding": finding})
|
|
}
|
|
|
|
// ResolveFinding resolves a finding with a resolution description
|
|
// POST /sdk/v1/vendors/findings/:id/resolve
|
|
func (h *VendorHandlers) ResolveFinding(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
userID := rbac.GetUserID(c)
|
|
id := c.Param("id")
|
|
|
|
var req vendor.ResolveFindingRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := h.store.ResolveFinding(c.Request.Context(), tenantID.String(), id, req.Resolution, userID.String()); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "finding resolved"})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Control Instance Operations
|
|
// ============================================================================
|
|
|
|
// UpsertControlInstance creates or updates a control instance
|
|
// POST /sdk/v1/vendors/controls
|
|
func (h *VendorHandlers) UpsertControlInstance(c *gin.Context) {
|
|
var req struct {
|
|
VendorID string `json:"vendor_id" binding:"required"`
|
|
ControlID string `json:"control_id" binding:"required"`
|
|
ControlDomain string `json:"control_domain"`
|
|
Status vendor.ControlStatus `json:"status" binding:"required"`
|
|
EvidenceIDs json.RawMessage `json:"evidence_ids,omitempty"`
|
|
Notes string `json:"notes,omitempty"`
|
|
NextAssessmentDate *time.Time `json:"next_assessment_date,omitempty"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
userID := rbac.GetUserID(c)
|
|
now := time.Now().UTC()
|
|
userIDStr := userID.String()
|
|
|
|
ci := &vendor.ControlInstance{
|
|
TenantID: tenantID,
|
|
ControlID: req.ControlID,
|
|
ControlDomain: req.ControlDomain,
|
|
Status: req.Status,
|
|
EvidenceIDs: req.EvidenceIDs,
|
|
Notes: req.Notes,
|
|
LastAssessedAt: &now,
|
|
LastAssessedBy: &userIDStr,
|
|
NextAssessmentDate: req.NextAssessmentDate,
|
|
}
|
|
|
|
// Parse VendorID
|
|
vendorUUID, err := parseUUID(req.VendorID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid vendor_id"})
|
|
return
|
|
}
|
|
ci.VendorID = vendorUUID
|
|
|
|
if err := h.store.UpsertControlInstance(c.Request.Context(), ci); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"control_instance": ci})
|
|
}
|
|
|
|
// ListControlInstances lists control instances for a vendor
|
|
// GET /sdk/v1/vendors/controls
|
|
func (h *VendorHandlers) ListControlInstances(c *gin.Context) {
|
|
vendorID := c.Query("vendor_id")
|
|
if vendorID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "vendor_id query parameter is required"})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
instances, err := h.store.ListControlInstances(c.Request.Context(), tenantID.String(), vendorID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"control_instances": instances,
|
|
"total": len(instances),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Template Operations
|
|
// ============================================================================
|
|
|
|
// ListTemplates lists available templates
|
|
// GET /sdk/v1/vendors/templates
|
|
func (h *VendorHandlers) ListTemplates(c *gin.Context) {
|
|
templateType := c.DefaultQuery("type", "VENDOR")
|
|
|
|
var category, industry *string
|
|
if cat := c.Query("category"); cat != "" {
|
|
category = &cat
|
|
}
|
|
if ind := c.Query("industry"); ind != "" {
|
|
industry = &ind
|
|
}
|
|
|
|
templates, err := h.store.ListTemplates(c.Request.Context(), templateType, category, industry)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"templates": templates,
|
|
"total": len(templates),
|
|
})
|
|
}
|
|
|
|
// GetTemplate retrieves a template by its template_id string
|
|
// GET /sdk/v1/vendors/templates/:templateId
|
|
func (h *VendorHandlers) GetTemplate(c *gin.Context) {
|
|
templateID := c.Param("templateId")
|
|
if templateID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "template ID is required"})
|
|
return
|
|
}
|
|
|
|
tmpl, err := h.store.GetTemplate(c.Request.Context(), templateID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if tmpl == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "template not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"template": tmpl})
|
|
}
|
|
|
|
// CreateTemplate creates a custom template
|
|
// POST /sdk/v1/vendors/templates
|
|
func (h *VendorHandlers) CreateTemplate(c *gin.Context) {
|
|
var req vendor.CreateTemplateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
tmpl := &vendor.Template{
|
|
TemplateType: req.TemplateType,
|
|
TemplateID: req.TemplateID,
|
|
Category: req.Category,
|
|
NameDE: req.NameDE,
|
|
NameEN: req.NameEN,
|
|
DescriptionDE: req.DescriptionDE,
|
|
DescriptionEN: req.DescriptionEN,
|
|
TemplateData: req.TemplateData,
|
|
Industry: req.Industry,
|
|
Tags: req.Tags,
|
|
IsSystem: req.IsSystem,
|
|
IsActive: true,
|
|
}
|
|
|
|
// Set tenant for custom (non-system) templates
|
|
if !req.IsSystem {
|
|
tid := rbac.GetTenantID(c).String()
|
|
tmpl.TenantID = &tid
|
|
}
|
|
|
|
if err := h.store.CreateTemplate(c.Request.Context(), tmpl); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"template": tmpl})
|
|
}
|
|
|
|
// ApplyTemplate creates a vendor from a template
|
|
// POST /sdk/v1/vendors/templates/:templateId/apply
|
|
func (h *VendorHandlers) ApplyTemplate(c *gin.Context) {
|
|
templateID := c.Param("templateId")
|
|
if templateID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "template ID is required"})
|
|
return
|
|
}
|
|
|
|
tmpl, err := h.store.GetTemplate(c.Request.Context(), templateID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if tmpl == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "template not found"})
|
|
return
|
|
}
|
|
|
|
// Parse template_data to extract suggested vendor fields
|
|
var templateData struct {
|
|
ServiceCategory string `json:"service_category"`
|
|
SuggestedRole string `json:"suggested_role"`
|
|
DataAccessLevel string `json:"data_access_level"`
|
|
ReviewFrequency string `json:"review_frequency"`
|
|
Certifications json.RawMessage `json:"certifications"`
|
|
ProcessingLocations json.RawMessage `json:"processing_locations"`
|
|
}
|
|
if err := json.Unmarshal(tmpl.TemplateData, &templateData); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse template data"})
|
|
return
|
|
}
|
|
|
|
// Optional overrides from request body
|
|
var overrides struct {
|
|
Name string `json:"name"`
|
|
Country string `json:"country"`
|
|
Website string `json:"website"`
|
|
ContactName string `json:"contact_name"`
|
|
ContactEmail string `json:"contact_email"`
|
|
}
|
|
c.ShouldBindJSON(&overrides)
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
userID := rbac.GetUserID(c)
|
|
|
|
v := &vendor.Vendor{
|
|
TenantID: tenantID,
|
|
Name: overrides.Name,
|
|
Country: overrides.Country,
|
|
Website: overrides.Website,
|
|
ContactName: overrides.ContactName,
|
|
ContactEmail: overrides.ContactEmail,
|
|
Role: vendor.VendorRole(templateData.SuggestedRole),
|
|
ServiceCategory: templateData.ServiceCategory,
|
|
DataAccessLevel: templateData.DataAccessLevel,
|
|
ReviewFrequency: templateData.ReviewFrequency,
|
|
Certifications: templateData.Certifications,
|
|
ProcessingLocations: templateData.ProcessingLocations,
|
|
Status: vendor.VendorStatusActive,
|
|
TemplateID: &templateID,
|
|
CreatedBy: userID.String(),
|
|
}
|
|
|
|
if v.Name == "" {
|
|
v.Name = tmpl.NameDE
|
|
}
|
|
if v.Country == "" {
|
|
v.Country = "DE"
|
|
}
|
|
if v.Role == "" {
|
|
v.Role = vendor.VendorRoleProcessor
|
|
}
|
|
|
|
if err := h.store.CreateVendor(c.Request.Context(), v); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Increment template usage
|
|
_ = h.store.IncrementTemplateUsage(c.Request.Context(), templateID)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"vendor": v,
|
|
"template_id": templateID,
|
|
"message": "vendor created from template",
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Statistics
|
|
// ============================================================================
|
|
|
|
// GetStatistics returns aggregated vendor statistics
|
|
// GET /sdk/v1/vendors/stats
|
|
func (h *VendorHandlers) GetStatistics(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
stats, err := h.store.GetVendorStats(c.Request.Context(), tenantID.String())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, stats)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
func parseUUID(s string) (uuid.UUID, error) {
|
|
return uuid.Parse(s)
|
|
}
|