Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/iace_handler_projects.go
Sharang Parnerkar 3f306fb6f0 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>
2026-04-19 09:17:20 +02:00

311 lines
9.5 KiB
Go

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"
)
// ============================================================================
// Project Management
// ============================================================================
// CreateProject handles POST /projects
// Creates a new IACE compliance project for a machine or system.
func (h *IACEHandler) CreateProject(c *gin.Context) {
tenantID, err := getTenantID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var req iace.CreateProjectRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
project, err := h.store.CreateProject(c.Request.Context(), tenantID, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"project": project})
}
// ListProjects handles GET /projects
// Lists all IACE projects for the authenticated tenant.
func (h *IACEHandler) ListProjects(c *gin.Context) {
tenantID, err := getTenantID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
projects, err := h.store.ListProjects(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if projects == nil {
projects = []iace.Project{}
}
c.JSON(http.StatusOK, iace.ProjectListResponse{
Projects: projects,
Total: len(projects),
})
}
// GetProject handles GET /projects/:id
// Returns a project with its components, classifications, and completeness gates.
func (h *IACEHandler) GetProject(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, _ := h.store.ListComponents(c.Request.Context(), projectID)
classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID)
if components == nil {
components = []iace.Component{}
}
if classifications == nil {
classifications = []iace.RegulatoryClassification{}
}
// Build completeness context to compute gates
ctx := h.buildCompletenessContext(c, project, components, classifications)
result := h.checker.Check(ctx)
c.JSON(http.StatusOK, iace.ProjectDetailResponse{
Project: *project,
Components: components,
Classifications: classifications,
CompletenessGates: result.Gates,
})
}
// UpdateProject handles PUT /projects/:id
// Partially updates a project's mutable fields.
func (h *IACEHandler) UpdateProject(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.UpdateProjectRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
project, err := h.store.UpdateProject(c.Request.Context(), projectID, req)
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
}
c.JSON(http.StatusOK, gin.H{"project": project})
}
// ArchiveProject handles DELETE /projects/:id
// Archives a project by setting its status to archived.
func (h *IACEHandler) ArchiveProject(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
if err := h.store.ArchiveProject(c.Request.Context(), projectID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "project archived"})
}
// ============================================================================
// Onboarding
// ============================================================================
// InitFromProfile handles POST /projects/:id/init-from-profile
// Initializes a project from a company profile and compliance scope JSON payload.
func (h *IACEHandler) InitFromProfile(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
}
var req iace.InitFromProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Parse compliance_scope to extract machine data
var scope struct {
MachineName string `json:"machine_name"`
MachineType string `json:"machine_type"`
IntendedUse string `json:"intended_use"`
HasSoftware bool `json:"has_software"`
HasFirmware bool `json:"has_firmware"`
HasAI bool `json:"has_ai"`
IsNetworked bool `json:"is_networked"`
ApplicableRegulations []string `json:"applicable_regulations"`
}
_ = json.Unmarshal(req.ComplianceScope, &scope)
// Parse company_profile to extract manufacturer
var profile struct {
CompanyName string `json:"company_name"`
ContactName string `json:"contact_name"`
ContactEmail string `json:"contact_email"`
Address string `json:"address"`
}
_ = json.Unmarshal(req.CompanyProfile, &profile)
// Store the profile and scope in project metadata
profileData := map[string]json.RawMessage{
"company_profile": req.CompanyProfile,
"compliance_scope": req.ComplianceScope,
}
metadataBytes, _ := json.Marshal(profileData)
metadataRaw := json.RawMessage(metadataBytes)
// Build update request — fill project fields from scope/profile
updateReq := iace.UpdateProjectRequest{
Metadata: &metadataRaw,
}
if scope.MachineName != "" {
updateReq.MachineName = &scope.MachineName
}
if scope.MachineType != "" {
updateReq.MachineType = &scope.MachineType
}
if scope.IntendedUse != "" {
updateReq.Description = &scope.IntendedUse
}
if profile.CompanyName != "" {
updateReq.Manufacturer = &profile.CompanyName
}
project, err = h.store.UpdateProject(c.Request.Context(), projectID, updateReq)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
// Create initial components from scope
var createdComponents []iace.Component
if scope.HasSoftware {
comp, err := h.store.CreateComponent(ctx, iace.CreateComponentRequest{
ProjectID: projectID, Name: "Software", ComponentType: iace.ComponentTypeSoftware,
IsSafetyRelevant: true, IsNetworked: scope.IsNetworked,
})
if err == nil {
createdComponents = append(createdComponents, *comp)
}
}
if scope.HasFirmware {
comp, err := h.store.CreateComponent(ctx, iace.CreateComponentRequest{
ProjectID: projectID, Name: "Firmware", ComponentType: iace.ComponentTypeFirmware,
IsSafetyRelevant: true,
})
if err == nil {
createdComponents = append(createdComponents, *comp)
}
}
if scope.HasAI {
comp, err := h.store.CreateComponent(ctx, iace.CreateComponentRequest{
ProjectID: projectID, Name: "KI-Modell", ComponentType: iace.ComponentTypeAIModel,
IsSafetyRelevant: true, IsNetworked: scope.IsNetworked,
})
if err == nil {
createdComponents = append(createdComponents, *comp)
}
}
if scope.IsNetworked {
comp, err := h.store.CreateComponent(ctx, iace.CreateComponentRequest{
ProjectID: projectID, Name: "Netzwerk-Schnittstelle", ComponentType: iace.ComponentTypeNetwork,
IsSafetyRelevant: false, IsNetworked: true,
})
if err == nil {
createdComponents = append(createdComponents, *comp)
}
}
// Trigger initial classifications for applicable regulations
regulationMap := map[string]iace.RegulationType{
"machinery_regulation": iace.RegulationMachineryRegulation,
"ai_act": iace.RegulationAIAct,
"cra": iace.RegulationCRA,
"nis2": iace.RegulationNIS2,
}
var triggeredRegulations []string
for _, regStr := range scope.ApplicableRegulations {
if regType, ok := regulationMap[regStr]; ok {
triggeredRegulations = append(triggeredRegulations, regStr)
// Create initial classification entry
h.store.UpsertClassification(ctx, projectID, regType, "pending", "medium", 0.5, "Initialisiert aus Compliance-Scope", nil, nil)
}
}
// Advance project status to onboarding
if err := h.store.UpdateProjectStatus(ctx, projectID, iace.ProjectStatusOnboarding); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Add audit trail entry
userID := rbac.GetUserID(c)
h.store.AddAuditEntry(
ctx, projectID, "project", projectID,
iace.AuditActionUpdate, userID.String(), nil, metadataBytes,
)
c.JSON(http.StatusOK, gin.H{
"message": "project initialized from profile",
"project": project,
"components_created": len(createdComponents),
"regulations_triggered": triggeredRegulations,
})
}