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