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>
227 lines
5.7 KiB
Go
227 lines
5.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"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"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Module Endpoints
|
|
// ============================================================================
|
|
|
|
// ListModules returns all training modules for the tenant
|
|
// GET /sdk/v1/training/modules
|
|
func (h *TrainingHandlers) ListModules(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
filters := &training.ModuleFilters{
|
|
Limit: 50,
|
|
Offset: 0,
|
|
}
|
|
|
|
if v := c.Query("regulation_area"); v != "" {
|
|
filters.RegulationArea = training.RegulationArea(v)
|
|
}
|
|
if v := c.Query("frequency_type"); v != "" {
|
|
filters.FrequencyType = training.FrequencyType(v)
|
|
}
|
|
if v := c.Query("search"); v != "" {
|
|
filters.Search = v
|
|
}
|
|
if v := c.Query("limit"); v != "" {
|
|
if n, err := strconv.Atoi(v); err == nil {
|
|
filters.Limit = n
|
|
}
|
|
}
|
|
if v := c.Query("offset"); v != "" {
|
|
if n, err := strconv.Atoi(v); err == nil {
|
|
filters.Offset = n
|
|
}
|
|
}
|
|
|
|
modules, total, err := h.store.ListModules(c.Request.Context(), tenantID, filters)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, training.ModuleListResponse{
|
|
Modules: modules,
|
|
Total: total,
|
|
})
|
|
}
|
|
|
|
// GetModule returns a single training module with content and quiz
|
|
// GET /sdk/v1/training/modules/:id
|
|
func (h *TrainingHandlers) GetModule(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"})
|
|
return
|
|
}
|
|
|
|
module, err := h.store.GetModule(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if module == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "module not found"})
|
|
return
|
|
}
|
|
|
|
// Include content and quiz questions
|
|
content, _ := h.store.GetPublishedContent(c.Request.Context(), id)
|
|
questions, _ := h.store.ListQuizQuestions(c.Request.Context(), id)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"module": module,
|
|
"content": content,
|
|
"questions": questions,
|
|
})
|
|
}
|
|
|
|
// CreateModule creates a new training module
|
|
// POST /sdk/v1/training/modules
|
|
func (h *TrainingHandlers) CreateModule(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
var req training.CreateModuleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
module := &training.TrainingModule{
|
|
TenantID: tenantID,
|
|
ModuleCode: req.ModuleCode,
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
RegulationArea: req.RegulationArea,
|
|
NIS2Relevant: req.NIS2Relevant,
|
|
ISOControls: req.ISOControls,
|
|
FrequencyType: req.FrequencyType,
|
|
ValidityDays: req.ValidityDays,
|
|
RiskWeight: req.RiskWeight,
|
|
ContentType: req.ContentType,
|
|
DurationMinutes: req.DurationMinutes,
|
|
PassThreshold: req.PassThreshold,
|
|
}
|
|
|
|
if module.ValidityDays == 0 {
|
|
module.ValidityDays = 365
|
|
}
|
|
if module.RiskWeight == 0 {
|
|
module.RiskWeight = 2.0
|
|
}
|
|
if module.ContentType == "" {
|
|
module.ContentType = "text"
|
|
}
|
|
if module.PassThreshold == 0 {
|
|
module.PassThreshold = 70
|
|
}
|
|
if module.ISOControls == nil {
|
|
module.ISOControls = []string{}
|
|
}
|
|
|
|
if err := h.store.CreateModule(c.Request.Context(), module); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, module)
|
|
}
|
|
|
|
// UpdateModule updates a training module
|
|
// PUT /sdk/v1/training/modules/:id
|
|
func (h *TrainingHandlers) UpdateModule(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"})
|
|
return
|
|
}
|
|
|
|
module, err := h.store.GetModule(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if module == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "module not found"})
|
|
return
|
|
}
|
|
|
|
var req training.UpdateModuleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if req.Title != nil {
|
|
module.Title = *req.Title
|
|
}
|
|
if req.Description != nil {
|
|
module.Description = *req.Description
|
|
}
|
|
if req.NIS2Relevant != nil {
|
|
module.NIS2Relevant = *req.NIS2Relevant
|
|
}
|
|
if req.ISOControls != nil {
|
|
module.ISOControls = req.ISOControls
|
|
}
|
|
if req.ValidityDays != nil {
|
|
module.ValidityDays = *req.ValidityDays
|
|
}
|
|
if req.RiskWeight != nil {
|
|
module.RiskWeight = *req.RiskWeight
|
|
}
|
|
if req.DurationMinutes != nil {
|
|
module.DurationMinutes = *req.DurationMinutes
|
|
}
|
|
if req.PassThreshold != nil {
|
|
module.PassThreshold = *req.PassThreshold
|
|
}
|
|
if req.IsActive != nil {
|
|
module.IsActive = *req.IsActive
|
|
}
|
|
|
|
if err := h.store.UpdateModule(c.Request.Context(), module); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, module)
|
|
}
|
|
|
|
// DeleteModule deletes a training module
|
|
// DELETE /sdk/v1/training/modules/:id
|
|
func (h *TrainingHandlers) DeleteModule(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"})
|
|
return
|
|
}
|
|
|
|
module, err := h.store.GetModule(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if module == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "module not found"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeleteModule(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
|
|
}
|