From 8087e74e88bdd92516a5e0c0877e6fbd41eb8add Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Fri, 8 May 2026 01:19:13 +0200 Subject: [PATCH] feat: Verification handler split + ListVerificationPlans Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/handlers/iace_handler_verification.go | 327 ++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 ai-compliance-sdk/internal/api/handlers/iace_handler_verification.go diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_verification.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_verification.go new file mode 100644 index 0000000..44d01a3 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_verification.go @@ -0,0 +1,327 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace" + "github.com/breakpilot/ai-compliance-sdk/internal/rbac" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// ============================================================================ +// Evidence & Verification Plans +// ============================================================================ + +// UploadEvidence handles POST /projects/:id/evidence +// Creates a new evidence record for a project. +func (h *IACEHandler) UploadEvidence(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + var req struct { + MitigationID *uuid.UUID `json:"mitigation_id,omitempty"` + VerificationPlanID *uuid.UUID `json:"verification_plan_id,omitempty"` + FileName string `json:"file_name" binding:"required"` + FilePath string `json:"file_path" binding:"required"` + FileHash string `json:"file_hash" binding:"required"` + FileSize int64 `json:"file_size" binding:"required"` + MimeType string `json:"mime_type" binding:"required"` + Description string `json:"description,omitempty"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := rbac.GetUserID(c) + + evidence := &iace.Evidence{ + ProjectID: projectID, + MitigationID: req.MitigationID, + VerificationPlanID: req.VerificationPlanID, + FileName: req.FileName, + FilePath: req.FilePath, + FileHash: req.FileHash, + FileSize: req.FileSize, + MimeType: req.MimeType, + Description: req.Description, + UploadedBy: userID, + } + + if err := h.store.CreateEvidence(c.Request.Context(), evidence); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + newVals, _ := json.Marshal(evidence) + h.store.AddAuditEntry( + c.Request.Context(), projectID, "evidence", evidence.ID, + iace.AuditActionCreate, userID.String(), nil, newVals, + ) + + c.JSON(http.StatusCreated, gin.H{"evidence": evidence}) +} + +// ListEvidence handles GET /projects/:id/evidence +func (h *IACEHandler) ListEvidence(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + evidence, err := h.store.ListEvidence(c.Request.Context(), projectID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if evidence == nil { + evidence = []iace.Evidence{} + } + + c.JSON(http.StatusOK, gin.H{"evidence": evidence, "total": len(evidence)}) +} + +// CreateVerificationPlan handles POST /projects/:id/verification-plan +func (h *IACEHandler) CreateVerificationPlan(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + var req iace.CreateVerificationPlanRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + req.ProjectID = projectID + + plan, err := h.store.CreateVerificationPlan(c.Request.Context(), req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + userID := rbac.GetUserID(c) + newVals, _ := json.Marshal(plan) + h.store.AddAuditEntry( + c.Request.Context(), projectID, "verification_plan", plan.ID, + iace.AuditActionCreate, userID.String(), nil, newVals, + ) + + c.JSON(http.StatusCreated, gin.H{"verification_plan": plan}) +} + +// UpdateVerificationPlan handles PUT /verification-plan/:vid +func (h *IACEHandler) UpdateVerificationPlan(c *gin.Context) { + planID, err := uuid.Parse(c.Param("vid")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"}) + return + } + + var updates map[string]interface{} + if err := c.ShouldBindJSON(&updates); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + plan, err := h.store.UpdateVerificationPlan(c.Request.Context(), planID, updates) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if plan == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "verification plan not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{"verification_plan": plan}) +} + +// CompleteVerification handles POST /verification-plan/:vid/complete +func (h *IACEHandler) CompleteVerification(c *gin.Context) { + planID, err := uuid.Parse(c.Param("vid")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"}) + return + } + + var req struct { + Result string `json:"result" binding:"required"` + Passed *bool `json:"passed"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := rbac.GetUserID(c) + + if err := h.store.CompleteVerification( + c.Request.Context(), planID, req.Result, userID.String(), + ); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "verification completed"}) +} + +// ============================================================================ +// Frontend-compatible /verifications routes +// ============================================================================ + +// ListVerificationPlans handles GET /projects/:id/verifications +func (h *IACEHandler) ListVerificationPlans(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + plans, err := h.store.ListVerificationPlans(c.Request.Context(), projectID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Enrich with linked hazard/mitigation names + hazards, _ := h.store.ListHazards(c.Request.Context(), projectID) + mitigations, _ := h.store.ListMitigationsByProject(c.Request.Context(), projectID) + hazardMap := make(map[uuid.UUID]string, len(hazards)) + for _, hz := range hazards { + hazardMap[hz.ID] = hz.Name + } + mitMap := make(map[uuid.UUID]string, len(mitigations)) + for _, m := range mitigations { + mitMap[m.ID] = m.Name + } + + type enrichedPlan struct { + iace.VerificationPlan + LinkedHazardName string `json:"linked_hazard_name,omitempty"` + LinkedMitigationName string `json:"linked_mitigation_name,omitempty"` + } + enriched := make([]enrichedPlan, 0, len(plans)) + for _, p := range plans { + ep := enrichedPlan{VerificationPlan: p} + if p.HazardID != nil { + ep.LinkedHazardName = hazardMap[*p.HazardID] + } + if p.MitigationID != nil { + ep.LinkedMitigationName = mitMap[*p.MitigationID] + } + enriched = append(enriched, ep) + } + + c.JSON(http.StatusOK, gin.H{"verifications": enriched, "total": len(enriched)}) +} + +// CreateVerificationAlias handles POST /projects/:id/verifications +// Frontend-compatible alias that maps linked_mitigation_id to mitigation_id. +func (h *IACEHandler) CreateVerificationAlias(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + var body struct { + Title string `json:"title" binding:"required"` + Description string `json:"description"` + Method string `json:"method" binding:"required"` + LinkedMitigationID *string `json:"linked_mitigation_id"` + LinkedHazardID *string `json:"linked_hazard_id"` + MitigationID *string `json:"mitigation_id"` + HazardID *string `json:"hazard_id"` + } + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + req := iace.CreateVerificationPlanRequest{ + ProjectID: projectID, + Title: body.Title, + Description: body.Description, + Method: iace.VerificationMethod(body.Method), + } + + midStr := body.MitigationID + if midStr == nil { + midStr = body.LinkedMitigationID + } + if midStr != nil { + mid, err := uuid.Parse(*midStr) + if err == nil { + req.MitigationID = &mid + } + } + + hidStr := body.HazardID + if hidStr == nil { + hidStr = body.LinkedHazardID + } + if hidStr != nil { + hid, err := uuid.Parse(*hidStr) + if err == nil { + req.HazardID = &hid + } + } + + plan, err := h.store.CreateVerificationPlan(c.Request.Context(), req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + userID := rbac.GetUserID(c) + newVals, _ := json.Marshal(plan) + h.store.AddAuditEntry( + c.Request.Context(), projectID, "verification_plan", plan.ID, + iace.AuditActionCreate, userID.String(), nil, newVals, + ) + + c.JSON(http.StatusCreated, plan) +} + +// DeleteVerificationPlan handles DELETE /projects/:id/verifications/:vid +func (h *IACEHandler) DeleteVerificationPlan(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + planID, err := uuid.Parse(c.Param("vid")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"}) + return + } + + if err := h.store.DeleteVerificationPlan(c.Request.Context(), planID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + userID := rbac.GetUserID(c) + h.store.AddAuditEntry( + c.Request.Context(), projectID, "verification_plan", planID, + iace.AuditActionDelete, userID.String(), nil, nil, + ) + + c.JSON(http.StatusOK, gin.H{"message": "verification plan deleted"}) +} + +// CompleteVerificationAlias handles POST /projects/:id/verifications/:vid/complete +func (h *IACEHandler) CompleteVerificationAlias(c *gin.Context) { + h.CompleteVerification(c) +}