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,325 @@
|
||||
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),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user