feat: Verification handler split + ListVerificationPlans
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user