- academy_handlers.go (1046 LOC) → academy_handlers.go (228) + academy_enrollment_handlers.go (320) + academy_generation_handlers.go (472) - workshop_handlers.go (923 LOC) → workshop_handlers.go (292) + workshop_interaction_handlers.go (452) + workshop_export_handlers.go (196) - content_generator.go (978 LOC) → content_generator.go (491) + content_generator_media.go (497) All files under 500 LOC hard cap. Zero behavior changes, no exported symbol renames. Both packages vet clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
229 lines
6.1 KiB
Go
229 lines
6.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/academy"
|
|
"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"
|
|
)
|
|
|
|
// AcademyHandlers handles academy HTTP requests
|
|
type AcademyHandlers struct {
|
|
store *academy.Store
|
|
trainingStore *training.Store
|
|
}
|
|
|
|
// NewAcademyHandlers creates new academy handlers
|
|
func NewAcademyHandlers(store *academy.Store, trainingStore *training.Store) *AcademyHandlers {
|
|
return &AcademyHandlers{store: store, trainingStore: trainingStore}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Course Management
|
|
// ============================================================================
|
|
|
|
// CreateCourse creates a new compliance training course
|
|
// POST /sdk/v1/academy/courses
|
|
func (h *AcademyHandlers) CreateCourse(c *gin.Context) {
|
|
var req academy.CreateCourseRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
course := &academy.Course{
|
|
TenantID: tenantID,
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
Category: req.Category,
|
|
DurationMinutes: req.DurationMinutes,
|
|
RequiredForRoles: req.RequiredForRoles,
|
|
IsActive: true,
|
|
}
|
|
|
|
if course.RequiredForRoles == nil {
|
|
course.RequiredForRoles = []string{}
|
|
}
|
|
|
|
if err := h.store.CreateCourse(c.Request.Context(), course); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Create lessons if provided
|
|
for i := range req.Lessons {
|
|
lesson := &academy.Lesson{
|
|
CourseID: course.ID,
|
|
Title: req.Lessons[i].Title,
|
|
Description: req.Lessons[i].Description,
|
|
LessonType: req.Lessons[i].LessonType,
|
|
ContentURL: req.Lessons[i].ContentURL,
|
|
DurationMinutes: req.Lessons[i].DurationMinutes,
|
|
OrderIndex: req.Lessons[i].OrderIndex,
|
|
QuizQuestions: req.Lessons[i].QuizQuestions,
|
|
}
|
|
if err := h.store.CreateLesson(c.Request.Context(), lesson); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
course.Lessons = append(course.Lessons, *lesson)
|
|
}
|
|
|
|
if course.Lessons == nil {
|
|
course.Lessons = []academy.Lesson{}
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"course": course})
|
|
}
|
|
|
|
// GetCourse retrieves a course with its lessons
|
|
// GET /sdk/v1/academy/courses/:id
|
|
func (h *AcademyHandlers) GetCourse(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid course ID"})
|
|
return
|
|
}
|
|
|
|
course, err := h.store.GetCourse(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if course == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "course not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"course": course})
|
|
}
|
|
|
|
// ListCourses lists courses for the current tenant
|
|
// GET /sdk/v1/academy/courses
|
|
func (h *AcademyHandlers) ListCourses(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
filters := &academy.CourseFilters{
|
|
Limit: 50,
|
|
}
|
|
|
|
if category := c.Query("category"); category != "" {
|
|
filters.Category = academy.CourseCategory(category)
|
|
}
|
|
if search := c.Query("search"); search != "" {
|
|
filters.Search = search
|
|
}
|
|
if activeStr := c.Query("is_active"); activeStr != "" {
|
|
active := activeStr == "true"
|
|
filters.IsActive = &active
|
|
}
|
|
if limitStr := c.Query("limit"); limitStr != "" {
|
|
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
|
|
filters.Limit = limit
|
|
}
|
|
}
|
|
if offsetStr := c.Query("offset"); offsetStr != "" {
|
|
if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 {
|
|
filters.Offset = offset
|
|
}
|
|
}
|
|
|
|
courses, total, err := h.store.ListCourses(c.Request.Context(), tenantID, filters)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, academy.CourseListResponse{
|
|
Courses: courses,
|
|
Total: total,
|
|
})
|
|
}
|
|
|
|
// UpdateCourse updates a course
|
|
// PUT /sdk/v1/academy/courses/:id
|
|
func (h *AcademyHandlers) UpdateCourse(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid course ID"})
|
|
return
|
|
}
|
|
|
|
course, err := h.store.GetCourse(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if course == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "course not found"})
|
|
return
|
|
}
|
|
|
|
var req academy.UpdateCourseRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if req.Title != nil {
|
|
course.Title = *req.Title
|
|
}
|
|
if req.Description != nil {
|
|
course.Description = *req.Description
|
|
}
|
|
if req.Category != nil {
|
|
course.Category = *req.Category
|
|
}
|
|
if req.DurationMinutes != nil {
|
|
course.DurationMinutes = *req.DurationMinutes
|
|
}
|
|
if req.RequiredForRoles != nil {
|
|
course.RequiredForRoles = req.RequiredForRoles
|
|
}
|
|
if req.IsActive != nil {
|
|
course.IsActive = *req.IsActive
|
|
}
|
|
|
|
if err := h.store.UpdateCourse(c.Request.Context(), course); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"course": course})
|
|
}
|
|
|
|
// DeleteCourse deletes a course
|
|
// DELETE /sdk/v1/academy/courses/:id
|
|
func (h *AcademyHandlers) DeleteCourse(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid course ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeleteCourse(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "course deleted"})
|
|
}
|
|
|
|
// estimateReadingTime estimates reading time in minutes from markdown content
|
|
// Average reading speed: ~200 words per minute
|
|
func estimateReadingTime(content string) int {
|
|
words := len(strings.Fields(content))
|
|
minutes := words / 200
|
|
if minutes < 5 {
|
|
minutes = 5
|
|
}
|
|
return minutes
|
|
}
|