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>
291 lines
8.1 KiB
Go
291 lines
8.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"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"
|
|
)
|
|
|
|
// ============================================================================
|
|
// 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,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Certificate Endpoints
|
|
// ============================================================================
|
|
|
|
// GenerateCertificate generates a certificate for a completed assignment
|
|
// POST /sdk/v1/training/certificates/generate/:assignmentId
|
|
func (h *TrainingHandlers) GenerateCertificate(c *gin.Context) {
|
|
assignmentID, err := uuid.Parse(c.Param("assignmentId"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
|
|
return
|
|
}
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
assignment, err := h.store.GetAssignment(c.Request.Context(), assignmentID)
|
|
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
|
|
}
|
|
|
|
if assignment.Status != training.AssignmentStatusCompleted {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "assignment is not completed"})
|
|
return
|
|
}
|
|
if assignment.QuizPassed == nil || !*assignment.QuizPassed {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "quiz has not been passed"})
|
|
return
|
|
}
|
|
|
|
// Generate certificate ID
|
|
certID := uuid.New()
|
|
if err := h.store.SetCertificateID(c.Request.Context(), assignmentID, certID); 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.AuditActionCertificateIssued,
|
|
EntityType: training.AuditEntityCertificate,
|
|
EntityID: &certID,
|
|
Details: map[string]interface{}{
|
|
"assignment_id": assignmentID.String(),
|
|
"user_name": assignment.UserName,
|
|
"module_title": assignment.ModuleTitle,
|
|
},
|
|
})
|
|
|
|
// Reload assignment with certificate_id
|
|
assignment, _ = h.store.GetAssignment(c.Request.Context(), assignmentID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"certificate_id": certID,
|
|
"assignment": assignment,
|
|
})
|
|
}
|
|
|
|
// DownloadCertificatePDF generates and returns a PDF certificate
|
|
// GET /sdk/v1/training/certificates/:id/pdf
|
|
func (h *TrainingHandlers) DownloadCertificatePDF(c *gin.Context) {
|
|
certID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid certificate ID"})
|
|
return
|
|
}
|
|
|
|
assignment, err := h.store.GetAssignmentByCertificateID(c.Request.Context(), certID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if assignment == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
|
return
|
|
}
|
|
|
|
// Get module for title
|
|
module, _ := h.store.GetModule(c.Request.Context(), assignment.ModuleID)
|
|
courseName := assignment.ModuleTitle
|
|
if module != nil {
|
|
courseName = module.Title
|
|
}
|
|
|
|
score := 0
|
|
if assignment.QuizScore != nil {
|
|
score = int(*assignment.QuizScore)
|
|
}
|
|
|
|
issuedAt := assignment.UpdatedAt
|
|
if assignment.CompletedAt != nil {
|
|
issuedAt = *assignment.CompletedAt
|
|
}
|
|
|
|
// Use academy PDF generator
|
|
pdfBytes, err := academy.GenerateCertificatePDF(academy.CertificateData{
|
|
CertificateID: certID.String(),
|
|
UserName: assignment.UserName,
|
|
CourseName: courseName,
|
|
Score: score,
|
|
IssuedAt: issuedAt,
|
|
ValidUntil: issuedAt.AddDate(1, 0, 0),
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "PDF generation failed: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.Header("Content-Disposition", "attachment; filename=zertifikat-"+certID.String()[:8]+".pdf")
|
|
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
|
}
|
|
|
|
// ListCertificates returns all certificates for a tenant
|
|
// GET /sdk/v1/training/certificates
|
|
func (h *TrainingHandlers) ListCertificates(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
certificates, err := h.store.ListCertificates(c.Request.Context(), tenantID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"certificates": certificates,
|
|
"total": len(certificates),
|
|
})
|
|
}
|