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,280 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/training"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Training Block Endpoints (Controls → Schulungsmodule)
|
||||
// ============================================================================
|
||||
|
||||
// ListBlockConfigs returns all block configs for the tenant
|
||||
// GET /sdk/v1/training/blocks
|
||||
func (h *TrainingHandlers) ListBlockConfigs(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
|
||||
configs, err := h.store.ListBlockConfigs(c.Request.Context(), tenantID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"blocks": configs,
|
||||
"total": len(configs),
|
||||
})
|
||||
}
|
||||
|
||||
// CreateBlockConfig creates a new block configuration
|
||||
// POST /sdk/v1/training/blocks
|
||||
func (h *TrainingHandlers) CreateBlockConfig(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
|
||||
var req training.CreateBlockConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
config := &training.TrainingBlockConfig{
|
||||
TenantID: tenantID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
DomainFilter: req.DomainFilter,
|
||||
CategoryFilter: req.CategoryFilter,
|
||||
SeverityFilter: req.SeverityFilter,
|
||||
TargetAudienceFilter: req.TargetAudienceFilter,
|
||||
RegulationArea: req.RegulationArea,
|
||||
ModuleCodePrefix: req.ModuleCodePrefix,
|
||||
FrequencyType: req.FrequencyType,
|
||||
DurationMinutes: req.DurationMinutes,
|
||||
PassThreshold: req.PassThreshold,
|
||||
MaxControlsPerModule: req.MaxControlsPerModule,
|
||||
}
|
||||
|
||||
if config.FrequencyType == "" {
|
||||
config.FrequencyType = training.FrequencyAnnual
|
||||
}
|
||||
if config.DurationMinutes == 0 {
|
||||
config.DurationMinutes = 45
|
||||
}
|
||||
if config.PassThreshold == 0 {
|
||||
config.PassThreshold = 70
|
||||
}
|
||||
if config.MaxControlsPerModule == 0 {
|
||||
config.MaxControlsPerModule = 20
|
||||
}
|
||||
|
||||
if err := h.store.CreateBlockConfig(c.Request.Context(), config); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, config)
|
||||
}
|
||||
|
||||
// GetBlockConfig returns a single block config
|
||||
// GET /sdk/v1/training/blocks/:id
|
||||
func (h *TrainingHandlers) GetBlockConfig(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
|
||||
return
|
||||
}
|
||||
|
||||
config, err := h.store.GetBlockConfig(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if config == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "block config not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, config)
|
||||
}
|
||||
|
||||
// UpdateBlockConfig updates a block config
|
||||
// PUT /sdk/v1/training/blocks/:id
|
||||
func (h *TrainingHandlers) UpdateBlockConfig(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
|
||||
return
|
||||
}
|
||||
|
||||
config, err := h.store.GetBlockConfig(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if config == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "block config not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req training.UpdateBlockConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
config.Name = *req.Name
|
||||
}
|
||||
if req.Description != nil {
|
||||
config.Description = *req.Description
|
||||
}
|
||||
if req.DomainFilter != nil {
|
||||
config.DomainFilter = *req.DomainFilter
|
||||
}
|
||||
if req.CategoryFilter != nil {
|
||||
config.CategoryFilter = *req.CategoryFilter
|
||||
}
|
||||
if req.SeverityFilter != nil {
|
||||
config.SeverityFilter = *req.SeverityFilter
|
||||
}
|
||||
if req.TargetAudienceFilter != nil {
|
||||
config.TargetAudienceFilter = *req.TargetAudienceFilter
|
||||
}
|
||||
if req.MaxControlsPerModule != nil {
|
||||
config.MaxControlsPerModule = *req.MaxControlsPerModule
|
||||
}
|
||||
if req.DurationMinutes != nil {
|
||||
config.DurationMinutes = *req.DurationMinutes
|
||||
}
|
||||
if req.PassThreshold != nil {
|
||||
config.PassThreshold = *req.PassThreshold
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
config.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
if err := h.store.UpdateBlockConfig(c.Request.Context(), config); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, config)
|
||||
}
|
||||
|
||||
// DeleteBlockConfig deletes a block config
|
||||
// DELETE /sdk/v1/training/blocks/:id
|
||||
func (h *TrainingHandlers) DeleteBlockConfig(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.DeleteBlockConfig(c.Request.Context(), id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
|
||||
}
|
||||
|
||||
// PreviewBlock performs a dry run showing matching controls and proposed roles
|
||||
// POST /sdk/v1/training/blocks/:id/preview
|
||||
func (h *TrainingHandlers) PreviewBlock(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
|
||||
return
|
||||
}
|
||||
|
||||
preview, err := h.blockGenerator.Preview(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, preview)
|
||||
}
|
||||
|
||||
// GenerateBlock runs the full generation pipeline
|
||||
// POST /sdk/v1/training/blocks/:id/generate
|
||||
func (h *TrainingHandlers) GenerateBlock(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req training.GenerateBlockRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
// Defaults are fine
|
||||
req.Language = "de"
|
||||
req.AutoMatrix = true
|
||||
}
|
||||
|
||||
result, err := h.blockGenerator.Generate(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetBlockControls returns control links for a block config
|
||||
// GET /sdk/v1/training/blocks/:id/controls
|
||||
func (h *TrainingHandlers) GetBlockControls(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
|
||||
return
|
||||
}
|
||||
|
||||
links, err := h.store.GetControlLinksForBlock(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"controls": links,
|
||||
"total": len(links),
|
||||
})
|
||||
}
|
||||
|
||||
// ListCanonicalControls returns filtered canonical controls for browsing
|
||||
// GET /sdk/v1/training/canonical/controls
|
||||
func (h *TrainingHandlers) ListCanonicalControls(c *gin.Context) {
|
||||
domain := c.Query("domain")
|
||||
category := c.Query("category")
|
||||
severity := c.Query("severity")
|
||||
targetAudience := c.Query("target_audience")
|
||||
|
||||
controls, err := h.store.QueryCanonicalControls(c.Request.Context(),
|
||||
domain, category, severity, targetAudience,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"controls": controls,
|
||||
"total": len(controls),
|
||||
})
|
||||
}
|
||||
|
||||
// GetCanonicalMeta returns aggregated metadata about canonical controls
|
||||
// GET /sdk/v1/training/canonical/meta
|
||||
func (h *TrainingHandlers) GetCanonicalMeta(c *gin.Context) {
|
||||
meta, err := h.store.GetCanonicalControlMeta(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, meta)
|
||||
}
|
||||
Reference in New Issue
Block a user