refactor(go/handlers): split iace_handler and training_handlers into focused files
iace_handler.go (2706 LOC) split into 9 files: - iace_handler.go: struct, constructor, shared helpers (~156 LOC) - iace_handler_projects.go: project CRUD + InitFromProfile (~310 LOC) - iace_handler_components.go: components + classification (~387 LOC) - iace_handler_hazards.go: hazard library, CRUD, risk assessment (~469 LOC) - iace_handler_mitigations.go: mitigations, evidence, verification plans (~293 LOC) - iace_handler_techfile.go: CE tech file generation/export (~452 LOC) - iace_handler_monitoring.go: monitoring events + audit trail (~134 LOC) - iace_handler_refdata.go: ISO 12100 ref data, patterns, suggestions (~465 LOC) - iace_handler_rag.go: RAG library search + section enrichment (~142 LOC) training_handlers.go (1864 LOC) split into 9 files: - training_handlers.go: struct + constructor (~23 LOC) - training_handlers_modules.go: module CRUD (~226 LOC) - training_handlers_matrix.go: CTM matrix endpoints (~95 LOC) - training_handlers_assignments.go: assignment lifecycle (~243 LOC) - training_handlers_quiz.go: quiz submit/grade/attempts (~185 LOC) - training_handlers_content.go: LLM content/audio/video generation (~274 LOC) - training_handlers_media.go: media, streaming, interactive video (~325 LOC) - training_handlers_blocks.go: block configs + canonical controls (~280 LOC) - training_handlers_stats.go: deadlines, escalation, audit, certificates (~290 LOC) All files remain in package handlers. Zero behavior changes. All exported function names preserved. All files under 500 LOC hard cap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Component Management
|
||||
// ============================================================================
|
||||
|
||||
// CreateComponent handles POST /projects/:id/components
|
||||
// Adds a new component to a project.
|
||||
func (h *IACEHandler) CreateComponent(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.CreateComponentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Override project ID from URL path
|
||||
req.ProjectID = projectID
|
||||
|
||||
component, err := h.store.CreateComponent(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Audit trail
|
||||
userID := rbac.GetUserID(c)
|
||||
newVals, _ := json.Marshal(component)
|
||||
h.store.AddAuditEntry(
|
||||
c.Request.Context(), projectID, "component", component.ID,
|
||||
iace.AuditActionCreate, userID.String(), nil, newVals,
|
||||
)
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"component": component})
|
||||
}
|
||||
|
||||
// ListComponents handles GET /projects/:id/components
|
||||
// Lists all components for a project.
|
||||
func (h *IACEHandler) ListComponents(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
components, err := h.store.ListComponents(c.Request.Context(), projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if components == nil {
|
||||
components = []iace.Component{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"components": components,
|
||||
"total": len(components),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateComponent handles PUT /projects/:id/components/:cid
|
||||
// Updates a component with the provided fields.
|
||||
func (h *IACEHandler) UpdateComponent(c *gin.Context) {
|
||||
_, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
componentID, err := uuid.Parse(c.Param("cid"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid component ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
component, err := h.store.UpdateComponent(c.Request.Context(), componentID, updates)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if component == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "component not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"component": component})
|
||||
}
|
||||
|
||||
// DeleteComponent handles DELETE /projects/:id/components/:cid
|
||||
// Deletes a component from a project.
|
||||
func (h *IACEHandler) DeleteComponent(c *gin.Context) {
|
||||
_, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
componentID, err := uuid.Parse(c.Param("cid"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid component ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.DeleteComponent(c.Request.Context(), componentID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "component deleted"})
|
||||
}
|
||||
|
||||
// CheckCompleteness handles POST /projects/:id/completeness-check
|
||||
// Loads all project data, evaluates all 25 CE completeness gates, updates the
|
||||
// project's completeness score, and returns the result.
|
||||
func (h *IACEHandler) CheckCompleteness(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if project == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Load all related entities
|
||||
components, _ := h.store.ListComponents(c.Request.Context(), projectID)
|
||||
classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID)
|
||||
hazards, _ := h.store.ListHazards(c.Request.Context(), projectID)
|
||||
|
||||
// Collect all assessments and mitigations across all hazards
|
||||
var allAssessments []iace.RiskAssessment
|
||||
var allMitigations []iace.Mitigation
|
||||
for _, hazard := range hazards {
|
||||
assessments, _ := h.store.ListAssessments(c.Request.Context(), hazard.ID)
|
||||
allAssessments = append(allAssessments, assessments...)
|
||||
|
||||
mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazard.ID)
|
||||
allMitigations = append(allMitigations, mitigations...)
|
||||
}
|
||||
|
||||
evidence, _ := h.store.ListEvidence(c.Request.Context(), projectID)
|
||||
techFileSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
||||
|
||||
// Determine if the project has AI components
|
||||
hasAI := false
|
||||
for _, comp := range components {
|
||||
if comp.ComponentType == iace.ComponentTypeAIModel {
|
||||
hasAI = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check audit trail for pattern matching
|
||||
patternMatchingPerformed, _ := h.store.HasAuditEntryForType(c.Request.Context(), projectID, "pattern_matching")
|
||||
|
||||
// Build completeness context
|
||||
completenessCtx := &iace.CompletenessContext{
|
||||
Project: project,
|
||||
Components: components,
|
||||
Classifications: classifications,
|
||||
Hazards: hazards,
|
||||
Assessments: allAssessments,
|
||||
Mitigations: allMitigations,
|
||||
Evidence: evidence,
|
||||
TechFileSections: techFileSections,
|
||||
HasAI: hasAI,
|
||||
PatternMatchingPerformed: patternMatchingPerformed,
|
||||
}
|
||||
|
||||
// Run the checker
|
||||
result := h.checker.Check(completenessCtx)
|
||||
|
||||
// Build risk summary for the project update
|
||||
riskSummary := map[string]int{
|
||||
"total_hazards": len(hazards),
|
||||
}
|
||||
for _, a := range allAssessments {
|
||||
riskSummary[string(a.RiskLevel)]++
|
||||
}
|
||||
|
||||
// Update project completeness score and risk summary
|
||||
if err := h.store.UpdateProjectCompleteness(
|
||||
c.Request.Context(), projectID, result.Score, riskSummary,
|
||||
); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"completeness": result,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Classification
|
||||
// ============================================================================
|
||||
|
||||
// Classify handles POST /projects/:id/classify
|
||||
// Runs all regulatory classifiers (AI Act, Machinery Regulation, CRA, NIS2),
|
||||
// upserts each result into the store, and returns classifications.
|
||||
func (h *IACEHandler) Classify(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if project == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
components, err := h.store.ListComponents(c.Request.Context(), projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Run all classifiers
|
||||
results := h.classifier.ClassifyAll(project, components)
|
||||
|
||||
// Upsert each classification result into the store
|
||||
var classifications []iace.RegulatoryClassification
|
||||
for _, r := range results {
|
||||
reqsJSON, _ := json.Marshal(r.Requirements)
|
||||
|
||||
classification, err := h.store.UpsertClassification(
|
||||
c.Request.Context(),
|
||||
projectID,
|
||||
r.Regulation,
|
||||
r.ClassificationResult,
|
||||
r.RiskLevel,
|
||||
r.Confidence,
|
||||
r.Reasoning,
|
||||
nil, // ragSources - not available from rule-based classifier
|
||||
reqsJSON,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if classification != nil {
|
||||
classifications = append(classifications, *classification)
|
||||
}
|
||||
}
|
||||
|
||||
// Advance project status to classification
|
||||
h.store.UpdateProjectStatus(c.Request.Context(), projectID, iace.ProjectStatusClassification)
|
||||
|
||||
// Audit trail
|
||||
userID := rbac.GetUserID(c)
|
||||
newVals, _ := json.Marshal(classifications)
|
||||
h.store.AddAuditEntry(
|
||||
c.Request.Context(), projectID, "classification", projectID,
|
||||
iace.AuditActionCreate, userID.String(), nil, newVals,
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"classifications": classifications,
|
||||
"total": len(classifications),
|
||||
})
|
||||
}
|
||||
|
||||
// GetClassifications handles GET /projects/:id/classifications
|
||||
// Returns all regulatory classifications for a project.
|
||||
func (h *IACEHandler) GetClassifications(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
classifications, err := h.store.GetClassifications(c.Request.Context(), projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if classifications == nil {
|
||||
classifications = []iace.RegulatoryClassification{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"classifications": classifications,
|
||||
"total": len(classifications),
|
||||
})
|
||||
}
|
||||
|
||||
// ClassifySingle handles POST /projects/:id/classify/:regulation
|
||||
// Runs a single regulatory classifier for the specified regulation type.
|
||||
func (h *IACEHandler) ClassifySingle(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
regulation := iace.RegulationType(c.Param("regulation"))
|
||||
|
||||
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if project == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
components, err := h.store.ListComponents(c.Request.Context(), projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Run the appropriate classifier
|
||||
var result iace.ClassificationResult
|
||||
switch regulation {
|
||||
case iace.RegulationAIAct:
|
||||
result = h.classifier.ClassifyAIAct(project, components)
|
||||
case iace.RegulationMachineryRegulation:
|
||||
result = h.classifier.ClassifyMachineryRegulation(project, components)
|
||||
case iace.RegulationCRA:
|
||||
result = h.classifier.ClassifyCRA(project, components)
|
||||
case iace.RegulationNIS2:
|
||||
result = h.classifier.ClassifyNIS2(project, components)
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown regulation type: %s", regulation)})
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert the classification result
|
||||
reqsJSON, _ := json.Marshal(result.Requirements)
|
||||
|
||||
classification, err := h.store.UpsertClassification(
|
||||
c.Request.Context(),
|
||||
projectID,
|
||||
result.Regulation,
|
||||
result.ClassificationResult,
|
||||
result.RiskLevel,
|
||||
result.Confidence,
|
||||
result.Reasoning,
|
||||
nil,
|
||||
reqsJSON,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"classification": classification})
|
||||
}
|
||||
Reference in New Issue
Block a user