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,243 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Assignment Endpoints
|
||||
// ============================================================================
|
||||
|
||||
// ComputeAssignments computes assignments for a user based on roles
|
||||
// POST /sdk/v1/training/assignments/compute
|
||||
func (h *TrainingHandlers) ComputeAssignments(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
|
||||
var req training.ComputeAssignmentsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
trigger := req.Trigger
|
||||
if trigger == "" {
|
||||
trigger = "manual"
|
||||
}
|
||||
|
||||
assignments, err := training.ComputeAssignments(
|
||||
c.Request.Context(), h.store, tenantID,
|
||||
req.UserID, req.UserName, req.UserEmail, req.Roles, trigger,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"assignments": assignments,
|
||||
"created": len(assignments),
|
||||
})
|
||||
}
|
||||
|
||||
// ListAssignments returns assignments for the tenant
|
||||
// GET /sdk/v1/training/assignments
|
||||
func (h *TrainingHandlers) ListAssignments(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
|
||||
filters := &training.AssignmentFilters{
|
||||
Limit: 50,
|
||||
Offset: 0,
|
||||
}
|
||||
|
||||
if v := c.Query("user_id"); v != "" {
|
||||
if uid, err := uuid.Parse(v); err == nil {
|
||||
filters.UserID = &uid
|
||||
}
|
||||
}
|
||||
if v := c.Query("module_id"); v != "" {
|
||||
if mid, err := uuid.Parse(v); err == nil {
|
||||
filters.ModuleID = &mid
|
||||
}
|
||||
}
|
||||
if v := c.Query("role"); v != "" {
|
||||
filters.RoleCode = v
|
||||
}
|
||||
if v := c.Query("status"); v != "" {
|
||||
filters.Status = training.AssignmentStatus(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
|
||||
}
|
||||
}
|
||||
|
||||
assignments, total, err := h.store.ListAssignments(c.Request.Context(), tenantID, filters)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, training.AssignmentListResponse{
|
||||
Assignments: assignments,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAssignment returns a single assignment
|
||||
// GET /sdk/v1/training/assignments/:id
|
||||
func (h *TrainingHandlers) GetAssignment(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
assignment, err := h.store.GetAssignment(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if assignment == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "assignment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, assignment)
|
||||
}
|
||||
|
||||
// StartAssignment marks an assignment as started
|
||||
// POST /sdk/v1/training/assignments/:id/start
|
||||
func (h *TrainingHandlers) StartAssignment(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
|
||||
return
|
||||
}
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
|
||||
if err := h.store.UpdateAssignmentStatus(c.Request.Context(), id, training.AssignmentStatusInProgress, 0); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Audit log
|
||||
userID := rbac.GetUserID(c)
|
||||
h.store.LogAction(c.Request.Context(), &training.AuditLogEntry{
|
||||
TenantID: tenantID,
|
||||
UserID: &userID,
|
||||
Action: training.AuditActionStarted,
|
||||
EntityType: training.AuditEntityAssignment,
|
||||
EntityID: &id,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "in_progress"})
|
||||
}
|
||||
|
||||
// UpdateAssignmentProgress updates progress on an assignment
|
||||
// POST /sdk/v1/training/assignments/:id/progress
|
||||
func (h *TrainingHandlers) UpdateAssignmentProgress(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req training.UpdateAssignmentProgressRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
status := training.AssignmentStatusInProgress
|
||||
if req.Progress >= 100 {
|
||||
status = training.AssignmentStatusCompleted
|
||||
}
|
||||
|
||||
if err := h.store.UpdateAssignmentStatus(c.Request.Context(), id, status, req.Progress); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": string(status), "progress": req.Progress})
|
||||
}
|
||||
|
||||
// UpdateAssignment updates assignment fields (e.g. deadline)
|
||||
// PUT /sdk/v1/training/assignments/:id
|
||||
func (h *TrainingHandlers) UpdateAssignment(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Deadline *string `json:"deadline"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Deadline != nil {
|
||||
deadline, err := time.Parse(time.RFC3339, *req.Deadline)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid deadline format (use RFC3339)"})
|
||||
return
|
||||
}
|
||||
if err := h.store.UpdateAssignmentDeadline(c.Request.Context(), id, deadline); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
assignment, err := h.store.GetAssignment(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if assignment == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "assignment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, assignment)
|
||||
}
|
||||
|
||||
// CompleteAssignment marks an assignment as completed
|
||||
// POST /sdk/v1/training/assignments/:id/complete
|
||||
func (h *TrainingHandlers) CompleteAssignment(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
|
||||
return
|
||||
}
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
|
||||
if err := h.store.UpdateAssignmentStatus(c.Request.Context(), id, training.AssignmentStatusCompleted, 100); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
h.store.LogAction(c.Request.Context(), &training.AuditLogEntry{
|
||||
TenantID: tenantID,
|
||||
UserID: &userID,
|
||||
Action: training.AuditActionCompleted,
|
||||
EntityType: training.AuditEntityAssignment,
|
||||
EntityID: &id,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "completed"})
|
||||
}
|
||||
Reference in New Issue
Block a user