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) }