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" ) // TrainingHandlers handles training-related API requests type TrainingHandlers struct { store *training.Store contentGenerator *training.ContentGenerator } // NewTrainingHandlers creates new training handlers func NewTrainingHandlers(store *training.Store, contentGenerator *training.ContentGenerator) *TrainingHandlers { return &TrainingHandlers{ store: store, contentGenerator: contentGenerator, } } // ============================================================================ // 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) } // ============================================================================ // Matrix Endpoints // ============================================================================ // GetMatrix returns the full CTM for the tenant // GET /sdk/v1/training/matrix func (h *TrainingHandlers) GetMatrix(c *gin.Context) { tenantID := rbac.GetTenantID(c) entries, err := h.store.GetMatrixForTenant(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } resp := training.BuildMatrixResponse(entries) c.JSON(http.StatusOK, resp) } // GetMatrixForRole returns matrix entries for a specific role // GET /sdk/v1/training/matrix/:role func (h *TrainingHandlers) GetMatrixForRole(c *gin.Context) { tenantID := rbac.GetTenantID(c) role := c.Param("role") entries, err := h.store.GetMatrixForRole(c.Request.Context(), tenantID, role) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "role": role, "label": training.RoleLabels[role], "entries": entries, "total": len(entries), }) } // SetMatrixEntry creates or updates a CTM entry // POST /sdk/v1/training/matrix func (h *TrainingHandlers) SetMatrixEntry(c *gin.Context) { tenantID := rbac.GetTenantID(c) var req training.SetMatrixEntryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } entry := &training.TrainingMatrixEntry{ TenantID: tenantID, RoleCode: req.RoleCode, ModuleID: req.ModuleID, IsMandatory: req.IsMandatory, Priority: req.Priority, } if err := h.store.SetMatrixEntry(c.Request.Context(), entry); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, entry) } // DeleteMatrixEntry removes a CTM entry // DELETE /sdk/v1/training/matrix/:role/:moduleId func (h *TrainingHandlers) DeleteMatrixEntry(c *gin.Context) { tenantID := rbac.GetTenantID(c) role := c.Param("role") moduleID, err := uuid.Parse(c.Param("moduleId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"}) return } if err := h.store.DeleteMatrixEntry(c.Request.Context(), tenantID, role, moduleID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "deleted"}) } // ============================================================================ // 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}) } // 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"}) } // ============================================================================ // Quiz Endpoints // ============================================================================ // GetQuiz returns quiz questions for a module // GET /sdk/v1/training/quiz/:moduleId func (h *TrainingHandlers) GetQuiz(c *gin.Context) { moduleID, err := uuid.Parse(c.Param("moduleId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"}) return } questions, err := h.store.ListQuizQuestions(c.Request.Context(), moduleID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Strip correct_index for the student-facing response type safeQuestion struct { ID uuid.UUID `json:"id"` Question string `json:"question"` Options []string `json:"options"` Difficulty string `json:"difficulty"` } safe := make([]safeQuestion, len(questions)) for i, q := range questions { safe[i] = safeQuestion{ ID: q.ID, Question: q.Question, Options: q.Options, Difficulty: string(q.Difficulty), } } c.JSON(http.StatusOK, gin.H{ "questions": safe, "total": len(safe), }) } // SubmitQuiz submits quiz answers and returns the score // POST /sdk/v1/training/quiz/:moduleId/submit func (h *TrainingHandlers) SubmitQuiz(c *gin.Context) { moduleID, err := uuid.Parse(c.Param("moduleId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"}) return } tenantID := rbac.GetTenantID(c) var req training.SubmitTrainingQuizRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Get the correct answers questions, err := h.store.ListQuizQuestions(c.Request.Context(), moduleID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Build answer map questionMap := make(map[uuid.UUID]training.QuizQuestion) for _, q := range questions { questionMap[q.ID] = q } // Score the answers correctCount := 0 totalCount := len(req.Answers) scoredAnswers := make([]training.QuizAnswer, len(req.Answers)) for i, answer := range req.Answers { q, exists := questionMap[answer.QuestionID] correct := exists && answer.SelectedIndex == q.CorrectIndex scoredAnswers[i] = training.QuizAnswer{ QuestionID: answer.QuestionID, SelectedIndex: answer.SelectedIndex, Correct: correct, } if correct { correctCount++ } } score := float64(0) if totalCount > 0 { score = float64(correctCount) / float64(totalCount) * 100 } // Get module for pass threshold module, _ := h.store.GetModule(c.Request.Context(), moduleID) threshold := 70 if module != nil { threshold = module.PassThreshold } passed := score >= float64(threshold) // Record the attempt userID := rbac.GetUserID(c) attempt := &training.QuizAttempt{ AssignmentID: req.AssignmentID, UserID: userID, Answers: scoredAnswers, Score: score, Passed: passed, CorrectCount: correctCount, TotalCount: totalCount, DurationSeconds: req.DurationSeconds, } if err := h.store.CreateQuizAttempt(c.Request.Context(), attempt); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Update assignment quiz result // Count total attempts attempts, _ := h.store.ListQuizAttempts(c.Request.Context(), req.AssignmentID) h.store.UpdateAssignmentQuizResult(c.Request.Context(), req.AssignmentID, score, passed, len(attempts)) // Audit log h.store.LogAction(c.Request.Context(), &training.AuditLogEntry{ TenantID: tenantID, UserID: &userID, Action: training.AuditActionQuizSubmitted, EntityType: training.AuditEntityQuiz, EntityID: &attempt.ID, Details: map[string]interface{}{ "module_id": moduleID.String(), "score": score, "passed": passed, "correct_count": correctCount, "total_count": totalCount, }, }) c.JSON(http.StatusOK, training.SubmitTrainingQuizResponse{ AttemptID: attempt.ID, Score: score, Passed: passed, CorrectCount: correctCount, TotalCount: totalCount, Threshold: threshold, }) } // GetQuizAttempts returns quiz attempts for an assignment // GET /sdk/v1/training/quiz/attempts/:assignmentId func (h *TrainingHandlers) GetQuizAttempts(c *gin.Context) { assignmentID, err := uuid.Parse(c.Param("assignmentId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } attempts, err := h.store.ListQuizAttempts(c.Request.Context(), assignmentID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "attempts": attempts, "total": len(attempts), }) } // ============================================================================ // Content Endpoints // ============================================================================ // GenerateContent generates module content via LLM // POST /sdk/v1/training/content/generate func (h *TrainingHandlers) GenerateContent(c *gin.Context) { var req training.GenerateContentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } module, err := h.store.GetModule(c.Request.Context(), req.ModuleID) 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 } content, err := h.contentGenerator.GenerateModuleContent(c.Request.Context(), *module, req.Language) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, content) } // GenerateQuiz generates quiz questions via LLM // POST /sdk/v1/training/content/generate-quiz func (h *TrainingHandlers) GenerateQuiz(c *gin.Context) { var req training.GenerateQuizRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } module, err := h.store.GetModule(c.Request.Context(), req.ModuleID) 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 } count := req.Count if count <= 0 { count = 5 } questions, err := h.contentGenerator.GenerateQuizQuestions(c.Request.Context(), *module, count) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "questions": questions, "total": len(questions), }) } // GetContent returns published content for a module // GET /sdk/v1/training/content/:moduleId func (h *TrainingHandlers) GetContent(c *gin.Context) { moduleID, err := uuid.Parse(c.Param("moduleId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"}) return } content, err := h.store.GetPublishedContent(c.Request.Context(), moduleID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if content == nil { // Try latest unpublished content, err = h.store.GetLatestContent(c.Request.Context(), moduleID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } if content == nil { c.JSON(http.StatusNotFound, gin.H{"error": "no content found for this module"}) return } c.JSON(http.StatusOK, content) } // PublishContent publishes a content version // POST /sdk/v1/training/content/:id/publish func (h *TrainingHandlers) PublishContent(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid content ID"}) return } reviewedBy := rbac.GetUserID(c) if err := h.store.PublishContent(c.Request.Context(), id, reviewedBy); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "published"}) } // ============================================================================ // Deadline / Escalation Endpoints // ============================================================================ // GetDeadlines returns upcoming deadlines // GET /sdk/v1/training/deadlines func (h *TrainingHandlers) GetDeadlines(c *gin.Context) { tenantID := rbac.GetTenantID(c) limit := 20 if v := c.Query("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil { limit = n } } deadlines, err := h.store.GetDeadlines(c.Request.Context(), tenantID, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, training.DeadlineListResponse{ Deadlines: deadlines, Total: len(deadlines), }) } // GetOverdueDeadlines returns overdue assignments // GET /sdk/v1/training/deadlines/overdue func (h *TrainingHandlers) GetOverdueDeadlines(c *gin.Context) { tenantID := rbac.GetTenantID(c) deadlines, err := training.GetOverdueDeadlines(c.Request.Context(), h.store, tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, training.DeadlineListResponse{ Deadlines: deadlines, Total: len(deadlines), }) } // CheckEscalation runs the escalation check // POST /sdk/v1/training/escalation/check func (h *TrainingHandlers) CheckEscalation(c *gin.Context) { tenantID := rbac.GetTenantID(c) results, err := training.CheckEscalations(c.Request.Context(), h.store, tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } overdueAll, _ := h.store.ListOverdueAssignments(c.Request.Context(), tenantID) c.JSON(http.StatusOK, training.EscalationResponse{ Results: results, TotalChecked: len(overdueAll), Escalated: len(results), }) } // ============================================================================ // Audit / Stats Endpoints // ============================================================================ // GetAuditLog returns the training audit trail // GET /sdk/v1/training/audit-log func (h *TrainingHandlers) GetAuditLog(c *gin.Context) { tenantID := rbac.GetTenantID(c) filters := &training.AuditLogFilters{ Limit: 50, Offset: 0, } if v := c.Query("action"); v != "" { filters.Action = training.AuditAction(v) } if v := c.Query("entity_type"); v != "" { filters.EntityType = training.AuditEntityType(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 } } entries, total, err := h.store.ListAuditLog(c.Request.Context(), tenantID, filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, training.AuditLogResponse{ Entries: entries, Total: total, }) } // GetStats returns training dashboard statistics // GET /sdk/v1/training/stats func (h *TrainingHandlers) GetStats(c *gin.Context) { tenantID := rbac.GetTenantID(c) stats, err := h.store.GetTrainingStats(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) } // VerifyCertificate verifies a certificate // GET /sdk/v1/training/certificates/:id/verify func (h *TrainingHandlers) VerifyCertificate(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid certificate ID"}) return } valid, assignment, err := training.VerifyCertificate(c.Request.Context(), h.store, id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"}) return } c.JSON(http.StatusOK, gin.H{ "valid": valid, "assignment": assignment, }) } // GenerateAllContent generates content for all modules that don't have content yet // POST /sdk/v1/training/content/generate-all func (h *TrainingHandlers) GenerateAllContent(c *gin.Context) { tenantID := rbac.GetTenantID(c) language := "de" if v := c.Query("language"); v != "" { language = v } result, err := h.contentGenerator.GenerateAllModuleContent(c.Request.Context(), tenantID, language) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } // GenerateAllQuizzes generates quiz questions for all modules that don't have questions yet // POST /sdk/v1/training/content/generate-all-quiz func (h *TrainingHandlers) GenerateAllQuizzes(c *gin.Context) { tenantID := rbac.GetTenantID(c) count := 5 result, err := h.contentGenerator.GenerateAllQuizQuestions(c.Request.Context(), tenantID, count) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } // GenerateAudio generates audio for a module via TTS service // POST /sdk/v1/training/content/:moduleId/generate-audio func (h *TrainingHandlers) GenerateAudio(c *gin.Context) { moduleID, err := uuid.Parse(c.Param("moduleId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"}) return } module, err := h.store.GetModule(c.Request.Context(), moduleID) 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 } media, err := h.contentGenerator.GenerateAudio(c.Request.Context(), *module) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, media) } // GetModuleMedia returns all media files for a module // GET /sdk/v1/training/media/:moduleId func (h *TrainingHandlers) GetModuleMedia(c *gin.Context) { moduleID, err := uuid.Parse(c.Param("moduleId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"}) return } mediaList, err := h.store.GetMediaForModule(c.Request.Context(), moduleID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "media": mediaList, "total": len(mediaList), }) } // GetMediaURL returns a presigned URL for a media file // GET /sdk/v1/training/media/:id/url func (h *TrainingHandlers) GetMediaURL(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid media ID"}) return } media, err := h.store.GetMedia(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if media == nil { c.JSON(http.StatusNotFound, gin.H{"error": "media not found"}) return } // Return the object info for the frontend to construct the URL c.JSON(http.StatusOK, gin.H{ "bucket": media.Bucket, "object_key": media.ObjectKey, "mime_type": media.MimeType, }) } // PublishMedia publishes or unpublishes a media file // POST /sdk/v1/training/media/:id/publish func (h *TrainingHandlers) PublishMedia(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid media ID"}) return } var req struct { Publish bool `json:"publish"` } if err := c.ShouldBindJSON(&req); err != nil { req.Publish = true // Default to publish } if err := h.store.PublishMedia(c.Request.Context(), id, req.Publish); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "ok", "is_published": req.Publish}) } // GenerateVideo generates a presentation video for a module // POST /sdk/v1/training/content/:moduleId/generate-video func (h *TrainingHandlers) GenerateVideo(c *gin.Context) { moduleID, err := uuid.Parse(c.Param("moduleId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"}) return } module, err := h.store.GetModule(c.Request.Context(), moduleID) 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 } media, err := h.contentGenerator.GenerateVideo(c.Request.Context(), *module) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, media) } // PreviewVideoScript generates and returns a video script preview without creating the video // POST /sdk/v1/training/content/:moduleId/preview-script func (h *TrainingHandlers) PreviewVideoScript(c *gin.Context) { moduleID, err := uuid.Parse(c.Param("moduleId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"}) return } module, err := h.store.GetModule(c.Request.Context(), moduleID) 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 } script, err := h.contentGenerator.GenerateVideoScript(c.Request.Context(), *module) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, script) }