8682522212
Backend: - parent_project_id auf iace_projects (DB + Go Struct) - POST/GET /variants + GET /variant-gap Endpoints - GAP-Analyse: Differenz Hazards/Massnahmen/Kategorien Frontend: - VariantPanel auf Projekt-Uebersicht - Variante erstellen Dialog - Sidebar-Anzeige (Variantenanzahl / Basis-Link) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
396 lines
12 KiB
Go
396 lines
12 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,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Variant Management
|
|
// ============================================================================
|
|
|
|
// CreateVariant handles POST /projects/:id/variants
|
|
// Creates a new variant sub-project linked to the given base project.
|
|
func (h *IACEHandler) CreateVariant(c *gin.Context) {
|
|
parentID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
// Verify parent project exists
|
|
parent, err := h.store.GetProject(c.Request.Context(), parentID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if parent == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "parent project not found"})
|
|
return
|
|
}
|
|
|
|
var req iace.CreateProjectRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Force the parent link
|
|
req.ParentProjectID = &parentID
|
|
|
|
project, err := h.store.CreateProject(c.Request.Context(), parent.TenantID, req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"project": project})
|
|
}
|
|
|
|
// ListVariants handles GET /projects/:id/variants
|
|
// Returns all variant sub-projects for a given base project.
|
|
func (h *IACEHandler) ListVariants(c *gin.Context) {
|
|
parentID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
variants, err := h.store.ListVariants(c.Request.Context(), parentID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if variants == nil {
|
|
variants = []iace.Project{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, iace.ProjectListResponse{
|
|
Projects: variants,
|
|
Total: len(variants),
|
|
})
|
|
}
|
|
|
|
// GetVariantGap handles GET /projects/:id/variant-gap
|
|
// Returns a gap analysis comparing a variant against its base project.
|
|
func (h *IACEHandler) GetVariantGap(c *gin.Context) {
|
|
variantID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
gap, err := h.store.GetVariantGap(c.Request.Context(), variantID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gap)
|
|
}
|