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" ) // ============================================================================ // Media Endpoints // ============================================================================ // 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}) } // StreamMedia returns a redirect to a presigned URL for a media file // GET /sdk/v1/training/media/:mediaId/stream func (h *TrainingHandlers) StreamMedia(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 } if h.ttsClient == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "media streaming not available"}) return } url, err := h.ttsClient.GetPresignedURL(c.Request.Context(), media.Bucket, media.ObjectKey) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate streaming URL: " + err.Error()}) return } c.Redirect(http.StatusTemporaryRedirect, url) } // ============================================================================ // Interactive Video Endpoints // ============================================================================ // GetInteractiveManifest returns the interactive video manifest with checkpoints and progress // GET /sdk/v1/training/content/:moduleId/interactive-manifest func (h *TrainingHandlers) GetInteractiveManifest(c *gin.Context) { moduleID, err := uuid.Parse(c.Param("moduleId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"}) return } // Get interactive video media mediaList, err := h.store.GetMediaForModule(c.Request.Context(), moduleID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Find interactive video var interactiveMedia *training.TrainingMedia for i := range mediaList { if mediaList[i].MediaType == training.MediaTypeInteractiveVideo && mediaList[i].Status == training.MediaStatusCompleted { interactiveMedia = &mediaList[i] break } } if interactiveMedia == nil { c.JSON(http.StatusNotFound, gin.H{"error": "no interactive video found for this module"}) return } // Get checkpoints checkpoints, err := h.store.ListCheckpoints(c.Request.Context(), moduleID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Optional: get assignment ID for progress assignmentIDStr := c.Query("assignment_id") // Build manifest entries entries := make([]training.CheckpointManifestEntry, len(checkpoints)) for i, cp := range checkpoints { // Get questions for this checkpoint questions, _ := h.store.GetCheckpointQuestions(c.Request.Context(), cp.ID) cpQuestions := make([]training.CheckpointQuestion, len(questions)) for j, q := range questions { cpQuestions[j] = training.CheckpointQuestion{ Question: q.Question, Options: q.Options, CorrectIndex: q.CorrectIndex, Explanation: q.Explanation, } } entry := training.CheckpointManifestEntry{ CheckpointID: cp.ID, Index: cp.CheckpointIndex, Title: cp.Title, TimestampSeconds: cp.TimestampSeconds, Questions: cpQuestions, } // Get progress if assignment_id provided if assignmentIDStr != "" { if assignmentID, err := uuid.Parse(assignmentIDStr); err == nil { progress, _ := h.store.GetCheckpointProgress(c.Request.Context(), assignmentID, cp.ID) entry.Progress = progress } } entries[i] = entry } // Get stream URL streamURL := "" if h.ttsClient != nil { url, err := h.ttsClient.GetPresignedURL(c.Request.Context(), interactiveMedia.Bucket, interactiveMedia.ObjectKey) if err == nil { streamURL = url } } manifest := training.InteractiveVideoManifest{ MediaID: interactiveMedia.ID, StreamURL: streamURL, Checkpoints: entries, } c.JSON(http.StatusOK, manifest) } // SubmitCheckpointQuiz handles checkpoint quiz submission // POST /sdk/v1/training/checkpoints/:checkpointId/submit func (h *TrainingHandlers) SubmitCheckpointQuiz(c *gin.Context) { checkpointID, err := uuid.Parse(c.Param("checkpointId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid checkpoint ID"}) return } var req training.SubmitCheckpointQuizRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } assignmentID, err := uuid.Parse(req.AssignmentID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } // Get checkpoint questions questions, err := h.store.GetCheckpointQuestions(c.Request.Context(), checkpointID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if len(questions) == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "no questions found for this checkpoint"}) return } // Grade answers correctCount := 0 feedback := make([]training.CheckpointQuizFeedback, len(questions)) for i, q := range questions { isCorrect := false if i < len(req.Answers) && req.Answers[i] == q.CorrectIndex { isCorrect = true correctCount++ } feedback[i] = training.CheckpointQuizFeedback{ Question: q.Question, Correct: isCorrect, Explanation: q.Explanation, } } score := float64(correctCount) / float64(len(questions)) * 100 passed := score >= 70 // 70% threshold for checkpoint // Update progress progress := &training.CheckpointProgress{ AssignmentID: assignmentID, CheckpointID: checkpointID, Passed: passed, Attempts: 1, } if err := h.store.UpsertCheckpointProgress(c.Request.Context(), progress); 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: rbac.GetTenantID(c), UserID: &userID, Action: training.AuditAction("checkpoint_submitted"), EntityType: training.AuditEntityType("checkpoint"), EntityID: &checkpointID, Details: map[string]interface{}{ "assignment_id": assignmentID.String(), "score": score, "passed": passed, "correct": correctCount, "total": len(questions), }, }) c.JSON(http.StatusOK, training.SubmitCheckpointQuizResponse{ Passed: passed, Score: score, Feedback: feedback, }) } // GetCheckpointProgress returns all checkpoint progress for an assignment // GET /sdk/v1/training/checkpoints/progress/:assignmentId func (h *TrainingHandlers) GetCheckpointProgress(c *gin.Context) { assignmentID, err := uuid.Parse(c.Param("assignmentId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"}) return } progress, err := h.store.ListCheckpointProgress(c.Request.Context(), assignmentID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "progress": progress, "total": len(progress), }) }