diff --git a/ai-compliance-sdk/cmd/server/main.go b/ai-compliance-sdk/cmd/server/main.go index bdcc79c..93cddce 100644 --- a/ai-compliance-sdk/cmd/server/main.go +++ b/ai-compliance-sdk/cmd/server/main.go @@ -1,681 +1,7 @@ package main -import ( - "context" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/breakpilot/ai-compliance-sdk/internal/api/handlers" - "github.com/breakpilot/ai-compliance-sdk/internal/audit" - "github.com/breakpilot/ai-compliance-sdk/internal/config" - "github.com/breakpilot/ai-compliance-sdk/internal/llm" - "github.com/breakpilot/ai-compliance-sdk/internal/rbac" - "github.com/breakpilot/ai-compliance-sdk/internal/academy" - "github.com/breakpilot/ai-compliance-sdk/internal/roadmap" - "github.com/breakpilot/ai-compliance-sdk/internal/training" - "github.com/breakpilot/ai-compliance-sdk/internal/ucca" - "github.com/breakpilot/ai-compliance-sdk/internal/whistleblower" - "github.com/breakpilot/ai-compliance-sdk/internal/iace" - "github.com/breakpilot/ai-compliance-sdk/internal/workshop" - "github.com/breakpilot/ai-compliance-sdk/internal/portfolio" - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" -) +import "github.com/breakpilot/ai-compliance-sdk/internal/app" func main() { - // Load configuration - cfg, err := config.Load() - if err != nil { - log.Fatalf("Failed to load configuration: %v", err) - } - - // Set Gin mode - if cfg.IsProduction() { - gin.SetMode(gin.ReleaseMode) - } - - // Connect to database - ctx := context.Background() - pool, err := pgxpool.New(ctx, cfg.DatabaseURL) - if err != nil { - log.Fatalf("Failed to connect to database: %v", err) - } - defer pool.Close() - - // Verify connection - if err := pool.Ping(ctx); err != nil { - log.Fatalf("Failed to ping database: %v", err) - } - log.Println("Connected to database") - - // Initialize stores - rbacStore := rbac.NewStore(pool) - auditStore := audit.NewStore(pool) - uccaStore := ucca.NewStore(pool) - escalationStore := ucca.NewEscalationStore(pool) - corpusVersionStore := ucca.NewCorpusVersionStore(pool) - roadmapStore := roadmap.NewStore(pool) - workshopStore := workshop.NewStore(pool) - portfolioStore := portfolio.NewStore(pool) - academyStore := academy.NewStore(pool) - whistleblowerStore := whistleblower.NewStore(pool) - iaceStore := iace.NewStore(pool) - trainingStore := training.NewStore(pool) - - // Initialize services - rbacService := rbac.NewService(rbacStore) - policyEngine := rbac.NewPolicyEngine(rbacService, rbacStore) - - // Initialize LLM providers - providerRegistry := llm.NewProviderRegistry(cfg.LLMProvider, cfg.LLMFallbackProvider) - - // Register Ollama adapter - ollamaAdapter := llm.NewOllamaAdapter(cfg.OllamaURL, cfg.OllamaDefaultModel) - providerRegistry.Register(ollamaAdapter) - - // Register Anthropic adapter if API key is configured - if cfg.AnthropicAPIKey != "" { - anthropicAdapter := llm.NewAnthropicAdapter(cfg.AnthropicAPIKey, cfg.AnthropicDefaultModel) - providerRegistry.Register(anthropicAdapter) - } - - // Initialize PII detector - piiDetector := llm.NewPIIDetectorWithPatterns(llm.AllPIIPatterns()) - - // Initialize TTS client and content generator for training - ttsClient := training.NewTTSClient(cfg.TTSServiceURL) - contentGenerator := training.NewContentGenerator(providerRegistry, piiDetector, trainingStore, ttsClient) - - // Initialize access gate - accessGate := llm.NewAccessGate(policyEngine, piiDetector, providerRegistry) - - // Initialize audit components - trailBuilder := audit.NewTrailBuilder(auditStore) - exporter := audit.NewExporter(auditStore) - - // Initialize handlers - rbacHandlers := handlers.NewRBACHandlers(rbacStore, rbacService, policyEngine) - llmHandlers := handlers.NewLLMHandlers(accessGate, providerRegistry, piiDetector, auditStore, trailBuilder) - auditHandlers := handlers.NewAuditHandlers(auditStore, exporter) - uccaHandlers := handlers.NewUCCAHandlers(uccaStore, escalationStore, providerRegistry) - escalationHandlers := handlers.NewEscalationHandlers(escalationStore, uccaStore) - roadmapHandlers := handlers.NewRoadmapHandlers(roadmapStore) - workshopHandlers := handlers.NewWorkshopHandlers(workshopStore) - portfolioHandlers := handlers.NewPortfolioHandlers(portfolioStore) - academyHandlers := handlers.NewAcademyHandlers(academyStore, trainingStore) - whistleblowerHandlers := handlers.NewWhistleblowerHandlers(whistleblowerStore) - iaceHandler := handlers.NewIACEHandler(iaceStore, providerRegistry) - blockGenerator := training.NewBlockGenerator(trainingStore, contentGenerator) - trainingHandlers := handlers.NewTrainingHandlers(trainingStore, contentGenerator, blockGenerator, ttsClient) - ragHandlers := handlers.NewRAGHandlers(corpusVersionStore) - - // Initialize obligations framework (v2 with TOM mapping) - obligationsStore := ucca.NewObligationsStore(pool) - obligationsHandlers := handlers.NewObligationsHandlersWithStore(obligationsStore) - - // Initialize middleware - rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine) - - // Create Gin router - router := gin.Default() - - // CORS configuration - router.Use(cors.New(cors.Config{ - AllowOrigins: cfg.AllowedOrigins, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "X-User-ID", "X-Tenant-ID", "X-Namespace-ID", "X-Tenant-Slug"}, - ExposeHeaders: []string{"Content-Length", "Content-Disposition"}, - AllowCredentials: true, - MaxAge: 12 * time.Hour, - })) - - // Health check - router.GET("/health", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "status": "healthy", - "timestamp": time.Now().UTC().Format(time.RFC3339), - }) - }) - - // API v1 routes - v1 := router.Group("/sdk/v1") - { - // Public routes (no auth required) - v1.GET("/health", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok"}) - }) - - // Apply user context extraction middleware - v1.Use(rbacMiddleware.ExtractUserContext()) - - // Tenant routes - tenants := v1.Group("/tenants") - { - tenants.GET("", rbacHandlers.ListTenants) - tenants.GET("/:id", rbacHandlers.GetTenant) - tenants.POST("", rbacHandlers.CreateTenant) - tenants.PUT("/:id", rbacHandlers.UpdateTenant) - - // Tenant namespaces (use :id to avoid route conflict) - tenants.GET("/:id/namespaces", rbacHandlers.ListNamespaces) - tenants.POST("/:id/namespaces", rbacHandlers.CreateNamespace) - } - - // Namespace routes - namespaces := v1.Group("/namespaces") - { - namespaces.GET("/:id", rbacHandlers.GetNamespace) - } - - // Role routes - roles := v1.Group("/roles") - { - roles.GET("", rbacHandlers.ListRoles) - roles.GET("/system", rbacHandlers.ListSystemRoles) - roles.GET("/:id", rbacHandlers.GetRole) - roles.POST("", rbacHandlers.CreateRole) - } - - // User role routes - userRoles := v1.Group("/user-roles") - { - userRoles.POST("", rbacHandlers.AssignRole) - userRoles.DELETE("/:userId/:roleId", rbacHandlers.RevokeRole) - userRoles.GET("/:userId", rbacHandlers.GetUserRoles) - } - - // Permission routes - permissions := v1.Group("/permissions") - { - permissions.GET("/effective", rbacHandlers.GetEffectivePermissions) - permissions.GET("/context", rbacHandlers.GetUserContext) - permissions.GET("/check", rbacHandlers.CheckPermission) - } - - // LLM Policy routes - policies := v1.Group("/llm/policies") - { - policies.GET("", rbacHandlers.ListLLMPolicies) - policies.GET("/:id", rbacHandlers.GetLLMPolicy) - policies.POST("", rbacHandlers.CreateLLMPolicy) - policies.PUT("/:id", rbacHandlers.UpdateLLMPolicy) - policies.DELETE("/:id", rbacHandlers.DeleteLLMPolicy) - } - - // LLM routes (require LLM permission) - llmRoutes := v1.Group("/llm") - llmRoutes.Use(rbacMiddleware.RequireLLMAccess()) - { - llmRoutes.POST("/chat", llmHandlers.Chat) - llmRoutes.POST("/complete", llmHandlers.Complete) - llmRoutes.GET("/models", llmHandlers.ListModels) - llmRoutes.GET("/providers/status", llmHandlers.GetProviderStatus) - llmRoutes.POST("/analyze", llmHandlers.AnalyzeText) - llmRoutes.POST("/redact", llmHandlers.RedactText) - } - - // Audit routes (require audit permission) - auditRoutes := v1.Group("/audit") - auditRoutes.Use(rbacMiddleware.RequireAnyPermission(rbac.PermissionAuditAll, rbac.PermissionAuditRead, rbac.PermissionAuditLogRead)) - { - auditRoutes.GET("/llm", auditHandlers.QueryLLMAudit) - auditRoutes.GET("/general", auditHandlers.QueryGeneralAudit) - auditRoutes.GET("/llm-operations", auditHandlers.QueryLLMAudit) // Alias - auditRoutes.GET("/trail", auditHandlers.QueryGeneralAudit) // Alias - auditRoutes.GET("/usage", auditHandlers.GetUsageStats) - auditRoutes.GET("/compliance-report", auditHandlers.GetComplianceReport) - - // Export routes - auditRoutes.GET("/export/llm", auditHandlers.ExportLLMAudit) - auditRoutes.GET("/export/general", auditHandlers.ExportGeneralAudit) - auditRoutes.GET("/export/compliance", auditHandlers.ExportComplianceReport) - } - - // UCCA routes - Use-Case Compliance & Feasibility Advisor - uccaRoutes := v1.Group("/ucca") - { - // Main assessment endpoint - uccaRoutes.POST("/assess", uccaHandlers.Assess) - - // Assessment management - uccaRoutes.GET("/assessments", uccaHandlers.ListAssessments) - uccaRoutes.GET("/assessments/:id", uccaHandlers.GetAssessment) - uccaRoutes.PUT("/assessments/:id", uccaHandlers.UpdateAssessment) - uccaRoutes.DELETE("/assessments/:id", uccaHandlers.DeleteAssessment) - - // LLM explanation - uccaRoutes.POST("/assessments/:id/explain", uccaHandlers.Explain) - - // Catalogs (patterns, examples, rules, controls, problem-solutions) - uccaRoutes.GET("/patterns", uccaHandlers.ListPatterns) - uccaRoutes.GET("/examples", uccaHandlers.ListExamples) - uccaRoutes.GET("/rules", uccaHandlers.ListRules) - uccaRoutes.GET("/controls", uccaHandlers.ListControls) - uccaRoutes.GET("/problem-solutions", uccaHandlers.ListProblemSolutions) - - // Export - uccaRoutes.GET("/export/:id", uccaHandlers.Export) - - // Escalation management (assessment-review workflows) - uccaRoutes.GET("/escalations", escalationHandlers.ListEscalations) - uccaRoutes.GET("/escalations/stats", escalationHandlers.GetEscalationStats) - uccaRoutes.GET("/escalations/:id", escalationHandlers.GetEscalation) - uccaRoutes.POST("/escalations", escalationHandlers.CreateEscalation) - uccaRoutes.POST("/escalations/:id/assign", escalationHandlers.AssignEscalation) - uccaRoutes.POST("/escalations/:id/review", escalationHandlers.StartReview) - uccaRoutes.POST("/escalations/:id/decide", escalationHandlers.DecideEscalation) - - // Obligations framework (v2 with TOM mapping) - obligationsHandlers.RegisterRoutes(uccaRoutes) - } - - // RAG routes - Legal Corpus Search & Versioning - ragRoutes := v1.Group("/rag") - { - ragRoutes.POST("/search", ragHandlers.Search) - ragRoutes.GET("/regulations", ragHandlers.ListRegulations) - ragRoutes.GET("/corpus-status", ragHandlers.CorpusStatus) - ragRoutes.GET("/corpus-versions/:collection", ragHandlers.CorpusVersionHistory) - ragRoutes.GET("/scroll", ragHandlers.HandleScrollChunks) - } - - // Roadmap routes - Compliance Implementation Roadmaps - roadmapRoutes := v1.Group("/roadmaps") - { - // Roadmap CRUD - roadmapRoutes.POST("", roadmapHandlers.CreateRoadmap) - roadmapRoutes.GET("", roadmapHandlers.ListRoadmaps) - roadmapRoutes.GET("/:id", roadmapHandlers.GetRoadmap) - roadmapRoutes.PUT("/:id", roadmapHandlers.UpdateRoadmap) - roadmapRoutes.DELETE("/:id", roadmapHandlers.DeleteRoadmap) - roadmapRoutes.GET("/:id/stats", roadmapHandlers.GetRoadmapStats) - - // Roadmap items - roadmapRoutes.POST("/:id/items", roadmapHandlers.CreateItem) - roadmapRoutes.GET("/:id/items", roadmapHandlers.ListItems) - - // Import workflow - roadmapRoutes.POST("/import/upload", roadmapHandlers.UploadImport) - roadmapRoutes.GET("/import/:jobId", roadmapHandlers.GetImportJob) - roadmapRoutes.POST("/import/:jobId/confirm", roadmapHandlers.ConfirmImport) - } - - // Roadmap item routes (separate group for item-level operations) - roadmapItemRoutes := v1.Group("/roadmap-items") - { - roadmapItemRoutes.GET("/:id", roadmapHandlers.GetItem) - roadmapItemRoutes.PUT("/:id", roadmapHandlers.UpdateItem) - roadmapItemRoutes.PATCH("/:id/status", roadmapHandlers.UpdateItemStatus) - roadmapItemRoutes.DELETE("/:id", roadmapHandlers.DeleteItem) - } - - // Workshop routes - Collaborative Compliance Workshops - workshopRoutes := v1.Group("/workshops") - { - // Session CRUD - workshopRoutes.POST("", workshopHandlers.CreateSession) - workshopRoutes.GET("", workshopHandlers.ListSessions) - workshopRoutes.GET("/:id", workshopHandlers.GetSession) - workshopRoutes.PUT("/:id", workshopHandlers.UpdateSession) - workshopRoutes.DELETE("/:id", workshopHandlers.DeleteSession) - - // Session lifecycle - workshopRoutes.POST("/:id/start", workshopHandlers.StartSession) - workshopRoutes.POST("/:id/pause", workshopHandlers.PauseSession) - workshopRoutes.POST("/:id/complete", workshopHandlers.CompleteSession) - - // Participants - workshopRoutes.GET("/:id/participants", workshopHandlers.ListParticipants) - workshopRoutes.PUT("/:id/participants/:participantId", workshopHandlers.UpdateParticipant) - workshopRoutes.DELETE("/:id/participants/:participantId", workshopHandlers.RemoveParticipant) - - // Responses - workshopRoutes.POST("/:id/responses", workshopHandlers.SubmitResponse) - workshopRoutes.GET("/:id/responses", workshopHandlers.GetResponses) - - // Comments - workshopRoutes.POST("/:id/comments", workshopHandlers.AddComment) - workshopRoutes.GET("/:id/comments", workshopHandlers.GetComments) - - // Wizard navigation - workshopRoutes.POST("/:id/advance", workshopHandlers.AdvanceStep) - workshopRoutes.POST("/:id/goto", workshopHandlers.GoToStep) - - // Statistics & Summary - workshopRoutes.GET("/:id/stats", workshopHandlers.GetSessionStats) - workshopRoutes.GET("/:id/summary", workshopHandlers.GetSessionSummary) - - // Export - workshopRoutes.GET("/:id/export", workshopHandlers.ExportSession) - - // Join by code (public endpoint for joining) - workshopRoutes.POST("/join/:code", workshopHandlers.JoinSession) - } - - // Portfolio routes - AI Use Case Portfolio Management - portfolioRoutes := v1.Group("/portfolios") - { - // Portfolio CRUD - portfolioRoutes.POST("", portfolioHandlers.CreatePortfolio) - portfolioRoutes.GET("", portfolioHandlers.ListPortfolios) - portfolioRoutes.GET("/:id", portfolioHandlers.GetPortfolio) - portfolioRoutes.PUT("/:id", portfolioHandlers.UpdatePortfolio) - portfolioRoutes.DELETE("/:id", portfolioHandlers.DeletePortfolio) - - // Portfolio items - portfolioRoutes.POST("/:id/items", portfolioHandlers.AddItem) - portfolioRoutes.GET("/:id/items", portfolioHandlers.ListItems) - portfolioRoutes.POST("/:id/items/bulk", portfolioHandlers.BulkAddItems) - portfolioRoutes.DELETE("/:id/items/:itemId", portfolioHandlers.RemoveItem) - portfolioRoutes.PUT("/:id/items/order", portfolioHandlers.ReorderItems) - - // Statistics & Activity - portfolioRoutes.GET("/:id/stats", portfolioHandlers.GetPortfolioStats) - portfolioRoutes.GET("/:id/activity", portfolioHandlers.GetPortfolioActivity) - portfolioRoutes.POST("/:id/recalculate", portfolioHandlers.RecalculateMetrics) - - // Approval workflow - portfolioRoutes.POST("/:id/submit-review", portfolioHandlers.SubmitForReview) - portfolioRoutes.POST("/:id/approve", portfolioHandlers.ApprovePortfolio) - - // Merge & Compare - portfolioRoutes.POST("/merge", portfolioHandlers.MergePortfolios) - portfolioRoutes.POST("/compare", portfolioHandlers.ComparePortfolios) - } - - // Academy routes - E-Learning / Compliance Training - academyRoutes := v1.Group("/academy") - { - // Courses - academyRoutes.POST("/courses", academyHandlers.CreateCourse) - academyRoutes.GET("/courses", academyHandlers.ListCourses) - academyRoutes.GET("/courses/:id", academyHandlers.GetCourse) - academyRoutes.PUT("/courses/:id", academyHandlers.UpdateCourse) - academyRoutes.DELETE("/courses/:id", academyHandlers.DeleteCourse) - - // Enrollments - academyRoutes.POST("/enrollments", academyHandlers.CreateEnrollment) - academyRoutes.GET("/enrollments", academyHandlers.ListEnrollments) - academyRoutes.PUT("/enrollments/:id/progress", academyHandlers.UpdateProgress) - academyRoutes.POST("/enrollments/:id/complete", academyHandlers.CompleteEnrollment) - - // Certificates - academyRoutes.GET("/certificates/:id", academyHandlers.GetCertificate) - academyRoutes.POST("/enrollments/:id/certificate", academyHandlers.GenerateCertificate) - - // Quiz - academyRoutes.POST("/courses/:id/quiz", academyHandlers.SubmitQuiz) - - // Lessons - academyRoutes.PUT("/lessons/:id", academyHandlers.UpdateLesson) - academyRoutes.POST("/lessons/:id/quiz-test", academyHandlers.TestQuiz) - - // Statistics - academyRoutes.GET("/stats", academyHandlers.GetStatistics) - - // Course Generation from Training Modules - academyRoutes.POST("/courses/generate", academyHandlers.GenerateCourseFromTraining) - academyRoutes.POST("/courses/generate-all", academyHandlers.GenerateAllCourses) - - // Certificate PDF - academyRoutes.GET("/certificates/:id/pdf", academyHandlers.DownloadCertificatePDF) - } - - // Training Engine routes - Compliance Training Content Pipeline - trainingRoutes := v1.Group("/training") - { - // Module CRUD - trainingRoutes.GET("/modules", trainingHandlers.ListModules) - trainingRoutes.GET("/modules/:id", trainingHandlers.GetModule) - trainingRoutes.POST("/modules", trainingHandlers.CreateModule) - trainingRoutes.PUT("/modules/:id", trainingHandlers.UpdateModule) - trainingRoutes.DELETE("/modules/:id", trainingHandlers.DeleteModule) - - // Compliance Training Matrix (CTM) - trainingRoutes.GET("/matrix", trainingHandlers.GetMatrix) - trainingRoutes.GET("/matrix/:role", trainingHandlers.GetMatrixForRole) - trainingRoutes.POST("/matrix", trainingHandlers.SetMatrixEntry) - trainingRoutes.DELETE("/matrix/:role/:moduleId", trainingHandlers.DeleteMatrixEntry) - - // Assignments - trainingRoutes.POST("/assignments/compute", trainingHandlers.ComputeAssignments) - trainingRoutes.GET("/assignments", trainingHandlers.ListAssignments) - trainingRoutes.GET("/assignments/:id", trainingHandlers.GetAssignment) - trainingRoutes.POST("/assignments/:id/start", trainingHandlers.StartAssignment) - trainingRoutes.POST("/assignments/:id/progress", trainingHandlers.UpdateAssignmentProgress) - trainingRoutes.POST("/assignments/:id/complete", trainingHandlers.CompleteAssignment) - trainingRoutes.PUT("/assignments/:id", trainingHandlers.UpdateAssignment) - - // Quiz - trainingRoutes.GET("/quiz/:moduleId", trainingHandlers.GetQuiz) - trainingRoutes.POST("/quiz/:moduleId/submit", trainingHandlers.SubmitQuiz) - trainingRoutes.GET("/quiz/attempts/:assignmentId", trainingHandlers.GetQuizAttempts) - - // Content Generation (LLM) - trainingRoutes.POST("/content/generate", trainingHandlers.GenerateContent) - trainingRoutes.POST("/content/generate-quiz", trainingHandlers.GenerateQuiz) - trainingRoutes.POST("/content/generate-all", trainingHandlers.GenerateAllContent) - trainingRoutes.POST("/content/generate-all-quiz", trainingHandlers.GenerateAllQuizzes) - trainingRoutes.GET("/content/:moduleId", trainingHandlers.GetContent) - // PublishContent expects c.Param("id") but route uses :moduleId for Gin compatibility - trainingRoutes.POST("/content/:moduleId/publish", func(c *gin.Context) { - c.Params = append(c.Params, gin.Param{Key: "id", Value: c.Param("moduleId")}) - trainingHandlers.PublishContent(c) - }) - - // Media (Audio/Video via TTS Service) - trainingRoutes.POST("/content/:moduleId/generate-audio", trainingHandlers.GenerateAudio) - trainingRoutes.POST("/content/:moduleId/generate-video", trainingHandlers.GenerateVideo) - trainingRoutes.POST("/content/:moduleId/preview-script", trainingHandlers.PreviewVideoScript) - trainingRoutes.GET("/media/module/:moduleId", trainingHandlers.GetModuleMedia) - // Media detail routes use :mediaId consistently - trainingRoutes.GET("/media/:mediaId/url", func(c *gin.Context) { - c.Params = append(c.Params, gin.Param{Key: "id", Value: c.Param("mediaId")}) - trainingHandlers.GetMediaURL(c) - }) - trainingRoutes.POST("/media/:mediaId/publish", func(c *gin.Context) { - c.Params = append(c.Params, gin.Param{Key: "id", Value: c.Param("mediaId")}) - trainingHandlers.PublishMedia(c) - }) - trainingRoutes.GET("/media/:mediaId/stream", func(c *gin.Context) { - c.Params = append(c.Params, gin.Param{Key: "id", Value: c.Param("mediaId")}) - trainingHandlers.StreamMedia(c) - }) - - // Deadlines & Escalation - trainingRoutes.GET("/deadlines", trainingHandlers.GetDeadlines) - trainingRoutes.GET("/deadlines/overdue", trainingHandlers.GetOverdueDeadlines) - trainingRoutes.POST("/escalation/check", trainingHandlers.CheckEscalation) - - // Audit & Statistics - trainingRoutes.GET("/audit-log", trainingHandlers.GetAuditLog) - trainingRoutes.GET("/stats", trainingHandlers.GetStats) - - // Certificates - trainingRoutes.POST("/certificates/generate/:assignmentId", trainingHandlers.GenerateCertificate) - trainingRoutes.GET("/certificates", trainingHandlers.ListCertificates) - trainingRoutes.GET("/certificates/:id/verify", trainingHandlers.VerifyCertificate) - trainingRoutes.GET("/certificates/:id/pdf", trainingHandlers.DownloadCertificatePDF) - - // Training Blocks — Controls → Schulungsmodule Pipeline - trainingRoutes.GET("/blocks", trainingHandlers.ListBlockConfigs) - trainingRoutes.POST("/blocks", trainingHandlers.CreateBlockConfig) - trainingRoutes.GET("/blocks/:id", trainingHandlers.GetBlockConfig) - trainingRoutes.PUT("/blocks/:id", trainingHandlers.UpdateBlockConfig) - trainingRoutes.DELETE("/blocks/:id", trainingHandlers.DeleteBlockConfig) - trainingRoutes.POST("/blocks/:id/preview", trainingHandlers.PreviewBlock) - trainingRoutes.POST("/blocks/:id/generate", trainingHandlers.GenerateBlock) - trainingRoutes.GET("/blocks/:id/controls", trainingHandlers.GetBlockControls) - - // Canonical Controls Browsing - trainingRoutes.GET("/canonical/controls", trainingHandlers.ListCanonicalControls) - trainingRoutes.GET("/canonical/meta", trainingHandlers.GetCanonicalMeta) - - // Interactive Video (Narrator + Checkpoints) - trainingRoutes.POST("/content/:moduleId/generate-interactive", trainingHandlers.GenerateInteractiveVideo) - trainingRoutes.GET("/content/:moduleId/interactive-manifest", trainingHandlers.GetInteractiveManifest) - trainingRoutes.POST("/checkpoints/:checkpointId/submit", trainingHandlers.SubmitCheckpointQuiz) - trainingRoutes.GET("/checkpoints/progress/:assignmentId", trainingHandlers.GetCheckpointProgress) - } - - // Whistleblower routes - Hinweisgebersystem (HinSchG) - whistleblowerRoutes := v1.Group("/whistleblower") - { - // Public endpoints (anonymous reporting) - whistleblowerRoutes.POST("/reports/submit", whistleblowerHandlers.SubmitReport) - whistleblowerRoutes.GET("/reports/access/:accessKey", whistleblowerHandlers.GetReportByAccessKey) - whistleblowerRoutes.POST("/reports/access/:accessKey/messages", whistleblowerHandlers.SendPublicMessage) - - // Admin endpoints - whistleblowerRoutes.GET("/reports", whistleblowerHandlers.ListReports) - whistleblowerRoutes.GET("/reports/:id", whistleblowerHandlers.GetReport) - whistleblowerRoutes.PUT("/reports/:id", whistleblowerHandlers.UpdateReport) - whistleblowerRoutes.DELETE("/reports/:id", whistleblowerHandlers.DeleteReport) - whistleblowerRoutes.POST("/reports/:id/acknowledge", whistleblowerHandlers.AcknowledgeReport) - whistleblowerRoutes.POST("/reports/:id/investigate", whistleblowerHandlers.StartInvestigation) - whistleblowerRoutes.POST("/reports/:id/measures", whistleblowerHandlers.AddMeasure) - whistleblowerRoutes.POST("/reports/:id/close", whistleblowerHandlers.CloseReport) - whistleblowerRoutes.POST("/reports/:id/messages", whistleblowerHandlers.SendAdminMessage) - whistleblowerRoutes.GET("/reports/:id/messages", whistleblowerHandlers.ListMessages) - - // Statistics - whistleblowerRoutes.GET("/stats", whistleblowerHandlers.GetStatistics) - } - - // IACE routes - Industrial AI Compliance Engine (CE-Risikobeurteilung SW/FW/KI) - iaceRoutes := v1.Group("/iace") - { - // Hazard Library (project-independent) - iaceRoutes.GET("/hazard-library", iaceHandler.ListHazardLibrary) - // Controls Library (project-independent) - iaceRoutes.GET("/controls-library", iaceHandler.ListControlsLibrary) - // ISO 12100 reference data (project-independent) - iaceRoutes.GET("/lifecycle-phases", iaceHandler.ListLifecyclePhases) - iaceRoutes.GET("/roles", iaceHandler.ListRoles) - iaceRoutes.GET("/evidence-types", iaceHandler.ListEvidenceTypes) - iaceRoutes.GET("/protective-measures-library", iaceHandler.ListProtectiveMeasures) - // Component Library & Energy Sources (Hazard Matching Engine) - iaceRoutes.GET("/component-library", iaceHandler.ListComponentLibrary) - iaceRoutes.GET("/energy-sources", iaceHandler.ListEnergySources) - // Tag Taxonomy - iaceRoutes.GET("/tags", iaceHandler.ListTags) - // Hazard Patterns - iaceRoutes.GET("/hazard-patterns", iaceHandler.ListHazardPatterns) - - // Project Management - iaceRoutes.POST("/projects", iaceHandler.CreateProject) - iaceRoutes.GET("/projects", iaceHandler.ListProjects) - iaceRoutes.GET("/projects/:id", iaceHandler.GetProject) - iaceRoutes.PUT("/projects/:id", iaceHandler.UpdateProject) - iaceRoutes.DELETE("/projects/:id", iaceHandler.ArchiveProject) - - // Onboarding - iaceRoutes.POST("/projects/:id/init-from-profile", iaceHandler.InitFromProfile) - iaceRoutes.POST("/projects/:id/completeness-check", iaceHandler.CheckCompleteness) - - // Components - iaceRoutes.POST("/projects/:id/components", iaceHandler.CreateComponent) - iaceRoutes.GET("/projects/:id/components", iaceHandler.ListComponents) - iaceRoutes.PUT("/projects/:id/components/:cid", iaceHandler.UpdateComponent) - iaceRoutes.DELETE("/projects/:id/components/:cid", iaceHandler.DeleteComponent) - - // Regulatory Classification - iaceRoutes.POST("/projects/:id/classify", iaceHandler.Classify) - iaceRoutes.GET("/projects/:id/classifications", iaceHandler.GetClassifications) - iaceRoutes.POST("/projects/:id/classify/:regulation", iaceHandler.ClassifySingle) - - // Hazards - iaceRoutes.POST("/projects/:id/hazards", iaceHandler.CreateHazard) - iaceRoutes.GET("/projects/:id/hazards", iaceHandler.ListHazards) - iaceRoutes.PUT("/projects/:id/hazards/:hid", iaceHandler.UpdateHazard) - iaceRoutes.POST("/projects/:id/hazards/suggest", iaceHandler.SuggestHazards) - - // Pattern Matching Engine - iaceRoutes.POST("/projects/:id/match-patterns", iaceHandler.MatchPatterns) - iaceRoutes.POST("/projects/:id/apply-patterns", iaceHandler.ApplyPatternResults) - iaceRoutes.POST("/projects/:id/hazards/:hid/suggest-measures", iaceHandler.SuggestMeasuresForHazard) - iaceRoutes.POST("/projects/:id/mitigations/:mid/suggest-evidence", iaceHandler.SuggestEvidenceForMitigation) - - // Risk Assessment - iaceRoutes.POST("/projects/:id/hazards/:hid/assess", iaceHandler.AssessRisk) - iaceRoutes.GET("/projects/:id/risk-summary", iaceHandler.GetRiskSummary) - iaceRoutes.POST("/projects/:id/hazards/:hid/reassess", iaceHandler.ReassessRisk) - - // Mitigations - iaceRoutes.POST("/projects/:id/hazards/:hid/mitigations", iaceHandler.CreateMitigation) - iaceRoutes.PUT("/mitigations/:mid", iaceHandler.UpdateMitigation) - iaceRoutes.POST("/mitigations/:mid/verify", iaceHandler.VerifyMitigation) - iaceRoutes.POST("/projects/:id/validate-mitigation-hierarchy", iaceHandler.ValidateMitigationHierarchy) - - // Evidence - iaceRoutes.POST("/projects/:id/evidence", iaceHandler.UploadEvidence) - iaceRoutes.GET("/projects/:id/evidence", iaceHandler.ListEvidence) - - // Verification Plans - iaceRoutes.POST("/projects/:id/verification-plan", iaceHandler.CreateVerificationPlan) - iaceRoutes.PUT("/verification-plan/:vid", iaceHandler.UpdateVerificationPlan) - iaceRoutes.POST("/verification-plan/:vid/complete", iaceHandler.CompleteVerification) - - // CE Technical File - iaceRoutes.POST("/projects/:id/tech-file/generate", iaceHandler.GenerateTechFile) - iaceRoutes.GET("/projects/:id/tech-file", iaceHandler.ListTechFileSections) - iaceRoutes.PUT("/projects/:id/tech-file/:section", iaceHandler.UpdateTechFileSection) - iaceRoutes.POST("/projects/:id/tech-file/:section/approve", iaceHandler.ApproveTechFileSection) - iaceRoutes.POST("/projects/:id/tech-file/:section/generate", iaceHandler.GenerateSingleSection) - iaceRoutes.GET("/projects/:id/tech-file/export", iaceHandler.ExportTechFile) - - // Monitoring - iaceRoutes.POST("/projects/:id/monitoring", iaceHandler.CreateMonitoringEvent) - iaceRoutes.GET("/projects/:id/monitoring", iaceHandler.ListMonitoringEvents) - iaceRoutes.PUT("/projects/:id/monitoring/:eid", iaceHandler.UpdateMonitoringEvent) - - // Audit Trail - iaceRoutes.GET("/projects/:id/audit-trail", iaceHandler.GetAuditTrail) - - // RAG Library Search (Phase 6) - iaceRoutes.POST("/library-search", iaceHandler.SearchLibrary) - iaceRoutes.POST("/projects/:id/tech-file/:section/enrich", iaceHandler.EnrichTechFileSection) - } - } - - // Create HTTP server - srv := &http.Server{ - Addr: ":" + cfg.Port, - Handler: router, - ReadTimeout: 30 * time.Second, - WriteTimeout: 5 * time.Minute, // LLM requests can take longer - IdleTimeout: 60 * time.Second, - } - - // Start server in goroutine - go func() { - log.Printf("AI Compliance SDK starting on port %s", cfg.Port) - log.Printf("Environment: %s", cfg.Environment) - log.Printf("Primary LLM Provider: %s", cfg.LLMProvider) - log.Printf("Fallback LLM Provider: %s", cfg.LLMFallbackProvider) - - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Server failed: %v", err) - } - }() - - // Graceful shutdown - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - log.Println("Shutting down server...") - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - if err := srv.Shutdown(ctx); err != nil { - log.Fatalf("Server forced to shutdown: %v", err) - } - - log.Println("Server exited") + app.Run() } diff --git a/ai-compliance-sdk/internal/academy/store_courses.go b/ai-compliance-sdk/internal/academy/store_courses.go new file mode 100644 index 0000000..6ff0902 --- /dev/null +++ b/ai-compliance-sdk/internal/academy/store_courses.go @@ -0,0 +1,349 @@ +package academy + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +// Store handles academy data persistence +type Store struct { + pool *pgxpool.Pool +} + +// NewStore creates a new academy store +func NewStore(pool *pgxpool.Pool) *Store { + return &Store{pool: pool} +} + +// ============================================================================ +// Course CRUD Operations +// ============================================================================ + +// CreateCourse creates a new course +func (s *Store) CreateCourse(ctx context.Context, course *Course) error { + course.ID = uuid.New() + course.CreatedAt = time.Now().UTC() + course.UpdatedAt = course.CreatedAt + if !course.IsActive { + course.IsActive = true + } + + requiredForRoles, _ := json.Marshal(course.RequiredForRoles) + + _, err := s.pool.Exec(ctx, ` + INSERT INTO academy_courses ( + id, tenant_id, title, description, category, + duration_minutes, required_for_roles, is_active, + created_at, updated_at + ) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, + $9, $10 + ) + `, + course.ID, course.TenantID, course.Title, course.Description, string(course.Category), + course.DurationMinutes, requiredForRoles, course.IsActive, + course.CreatedAt, course.UpdatedAt, + ) + + return err +} + +// GetCourse retrieves a course by ID +func (s *Store) GetCourse(ctx context.Context, id uuid.UUID) (*Course, error) { + var course Course + var category string + var requiredForRoles []byte + + err := s.pool.QueryRow(ctx, ` + SELECT + id, tenant_id, title, description, category, + duration_minutes, required_for_roles, is_active, + created_at, updated_at + FROM academy_courses WHERE id = $1 + `, id).Scan( + &course.ID, &course.TenantID, &course.Title, &course.Description, &category, + &course.DurationMinutes, &requiredForRoles, &course.IsActive, + &course.CreatedAt, &course.UpdatedAt, + ) + + if err == pgx.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + course.Category = CourseCategory(category) + json.Unmarshal(requiredForRoles, &course.RequiredForRoles) + if course.RequiredForRoles == nil { + course.RequiredForRoles = []string{} + } + + // Load lessons for this course + lessons, err := s.ListLessons(ctx, course.ID) + if err != nil { + return nil, err + } + course.Lessons = lessons + + return &course, nil +} + +// ListCourses lists courses for a tenant with optional filters +func (s *Store) ListCourses(ctx context.Context, tenantID uuid.UUID, filters *CourseFilters) ([]Course, int, error) { + // Count query + countQuery := "SELECT COUNT(*) FROM academy_courses WHERE tenant_id = $1" + countArgs := []interface{}{tenantID} + countArgIdx := 2 + + // List query + query := ` + SELECT + id, tenant_id, title, description, category, + duration_minutes, required_for_roles, is_active, + created_at, updated_at + FROM academy_courses WHERE tenant_id = $1` + + args := []interface{}{tenantID} + argIdx := 2 + + if filters != nil { + if filters.Category != "" { + query += fmt.Sprintf(" AND category = $%d", argIdx) + args = append(args, string(filters.Category)) + argIdx++ + + countQuery += fmt.Sprintf(" AND category = $%d", countArgIdx) + countArgs = append(countArgs, string(filters.Category)) + countArgIdx++ + } + if filters.IsActive != nil { + query += fmt.Sprintf(" AND is_active = $%d", argIdx) + args = append(args, *filters.IsActive) + argIdx++ + + countQuery += fmt.Sprintf(" AND is_active = $%d", countArgIdx) + countArgs = append(countArgs, *filters.IsActive) + countArgIdx++ + } + if filters.Search != "" { + query += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d)", argIdx, argIdx) + args = append(args, "%"+filters.Search+"%") + argIdx++ + + countQuery += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d)", countArgIdx, countArgIdx) + countArgs = append(countArgs, "%"+filters.Search+"%") + countArgIdx++ + } + } + + // Get total count + var total int + err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total) + if err != nil { + return nil, 0, err + } + + query += " ORDER BY created_at DESC" + + if filters != nil && filters.Limit > 0 { + query += fmt.Sprintf(" LIMIT $%d", argIdx) + args = append(args, filters.Limit) + argIdx++ + + if filters.Offset > 0 { + query += fmt.Sprintf(" OFFSET $%d", argIdx) + args = append(args, filters.Offset) + argIdx++ + } + } + + rows, err := s.pool.Query(ctx, query, args...) + if err != nil { + return nil, 0, err + } + defer rows.Close() + + var courses []Course + for rows.Next() { + var course Course + var category string + var requiredForRoles []byte + + err := rows.Scan( + &course.ID, &course.TenantID, &course.Title, &course.Description, &category, + &course.DurationMinutes, &requiredForRoles, &course.IsActive, + &course.CreatedAt, &course.UpdatedAt, + ) + if err != nil { + return nil, 0, err + } + + course.Category = CourseCategory(category) + json.Unmarshal(requiredForRoles, &course.RequiredForRoles) + if course.RequiredForRoles == nil { + course.RequiredForRoles = []string{} + } + + courses = append(courses, course) + } + + if courses == nil { + courses = []Course{} + } + + return courses, total, nil +} + +// UpdateCourse updates a course +func (s *Store) UpdateCourse(ctx context.Context, course *Course) error { + course.UpdatedAt = time.Now().UTC() + + requiredForRoles, _ := json.Marshal(course.RequiredForRoles) + + _, err := s.pool.Exec(ctx, ` + UPDATE academy_courses SET + title = $2, description = $3, category = $4, + duration_minutes = $5, required_for_roles = $6, is_active = $7, + updated_at = $8 + WHERE id = $1 + `, + course.ID, course.Title, course.Description, string(course.Category), + course.DurationMinutes, requiredForRoles, course.IsActive, + course.UpdatedAt, + ) + + return err +} + +// DeleteCourse deletes a course and its related data (via CASCADE) +func (s *Store) DeleteCourse(ctx context.Context, id uuid.UUID) error { + _, err := s.pool.Exec(ctx, "DELETE FROM academy_courses WHERE id = $1", id) + return err +} + +// ============================================================================ +// Lesson Operations +// ============================================================================ + +// CreateLesson creates a new lesson +func (s *Store) CreateLesson(ctx context.Context, lesson *Lesson) error { + lesson.ID = uuid.New() + + quizQuestions, _ := json.Marshal(lesson.QuizQuestions) + + _, err := s.pool.Exec(ctx, ` + INSERT INTO academy_lessons ( + id, course_id, title, description, lesson_type, + content_url, duration_minutes, order_index, quiz_questions + ) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, $9 + ) + `, + lesson.ID, lesson.CourseID, lesson.Title, lesson.Description, string(lesson.LessonType), + lesson.ContentURL, lesson.DurationMinutes, lesson.OrderIndex, quizQuestions, + ) + + return err +} + +// ListLessons lists lessons for a course ordered by order_index +func (s *Store) ListLessons(ctx context.Context, courseID uuid.UUID) ([]Lesson, error) { + rows, err := s.pool.Query(ctx, ` + SELECT + id, course_id, title, description, lesson_type, + content_url, duration_minutes, order_index, quiz_questions + FROM academy_lessons WHERE course_id = $1 + ORDER BY order_index ASC + `, courseID) + if err != nil { + return nil, err + } + defer rows.Close() + + var lessons []Lesson + for rows.Next() { + var lesson Lesson + var lessonType string + var quizQuestions []byte + + err := rows.Scan( + &lesson.ID, &lesson.CourseID, &lesson.Title, &lesson.Description, &lessonType, + &lesson.ContentURL, &lesson.DurationMinutes, &lesson.OrderIndex, &quizQuestions, + ) + if err != nil { + return nil, err + } + + lesson.LessonType = LessonType(lessonType) + json.Unmarshal(quizQuestions, &lesson.QuizQuestions) + if lesson.QuizQuestions == nil { + lesson.QuizQuestions = []QuizQuestion{} + } + + lessons = append(lessons, lesson) + } + + if lessons == nil { + lessons = []Lesson{} + } + + return lessons, nil +} + +// GetLesson retrieves a single lesson by ID +func (s *Store) GetLesson(ctx context.Context, id uuid.UUID) (*Lesson, error) { + var lesson Lesson + var lessonType string + var quizQuestions []byte + + err := s.pool.QueryRow(ctx, ` + SELECT + id, course_id, title, description, lesson_type, + content_url, duration_minutes, order_index, quiz_questions + FROM academy_lessons WHERE id = $1 + `, id).Scan( + &lesson.ID, &lesson.CourseID, &lesson.Title, &lesson.Description, &lessonType, + &lesson.ContentURL, &lesson.DurationMinutes, &lesson.OrderIndex, &quizQuestions, + ) + + if err == pgx.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + lesson.LessonType = LessonType(lessonType) + json.Unmarshal(quizQuestions, &lesson.QuizQuestions) + if lesson.QuizQuestions == nil { + lesson.QuizQuestions = []QuizQuestion{} + } + + return &lesson, nil +} + +// UpdateLesson updates a lesson's content, title, and quiz questions +func (s *Store) UpdateLesson(ctx context.Context, lesson *Lesson) error { + quizQuestions, _ := json.Marshal(lesson.QuizQuestions) + + _, err := s.pool.Exec(ctx, ` + UPDATE academy_lessons SET + title = $2, description = $3, content_url = $4, + duration_minutes = $5, quiz_questions = $6 + WHERE id = $1 + `, + lesson.ID, lesson.Title, lesson.Description, + lesson.ContentURL, lesson.DurationMinutes, quizQuestions, + ) + + return err +} diff --git a/ai-compliance-sdk/internal/academy/store.go b/ai-compliance-sdk/internal/academy/store_enrollments.go similarity index 51% rename from ai-compliance-sdk/internal/academy/store.go rename to ai-compliance-sdk/internal/academy/store_enrollments.go index 5e114b5..5a2bfc9 100644 --- a/ai-compliance-sdk/internal/academy/store.go +++ b/ai-compliance-sdk/internal/academy/store_enrollments.go @@ -2,352 +2,13 @@ package academy import ( "context" - "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" ) -// Store handles academy data persistence -type Store struct { - pool *pgxpool.Pool -} - -// NewStore creates a new academy store -func NewStore(pool *pgxpool.Pool) *Store { - return &Store{pool: pool} -} - -// ============================================================================ -// Course CRUD Operations -// ============================================================================ - -// CreateCourse creates a new course -func (s *Store) CreateCourse(ctx context.Context, course *Course) error { - course.ID = uuid.New() - course.CreatedAt = time.Now().UTC() - course.UpdatedAt = course.CreatedAt - if !course.IsActive { - course.IsActive = true - } - - requiredForRoles, _ := json.Marshal(course.RequiredForRoles) - - _, err := s.pool.Exec(ctx, ` - INSERT INTO academy_courses ( - id, tenant_id, title, description, category, - duration_minutes, required_for_roles, is_active, - created_at, updated_at - ) VALUES ( - $1, $2, $3, $4, $5, - $6, $7, $8, - $9, $10 - ) - `, - course.ID, course.TenantID, course.Title, course.Description, string(course.Category), - course.DurationMinutes, requiredForRoles, course.IsActive, - course.CreatedAt, course.UpdatedAt, - ) - - return err -} - -// GetCourse retrieves a course by ID -func (s *Store) GetCourse(ctx context.Context, id uuid.UUID) (*Course, error) { - var course Course - var category string - var requiredForRoles []byte - - err := s.pool.QueryRow(ctx, ` - SELECT - id, tenant_id, title, description, category, - duration_minutes, required_for_roles, is_active, - created_at, updated_at - FROM academy_courses WHERE id = $1 - `, id).Scan( - &course.ID, &course.TenantID, &course.Title, &course.Description, &category, - &course.DurationMinutes, &requiredForRoles, &course.IsActive, - &course.CreatedAt, &course.UpdatedAt, - ) - - if err == pgx.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, err - } - - course.Category = CourseCategory(category) - json.Unmarshal(requiredForRoles, &course.RequiredForRoles) - if course.RequiredForRoles == nil { - course.RequiredForRoles = []string{} - } - - // Load lessons for this course - lessons, err := s.ListLessons(ctx, course.ID) - if err != nil { - return nil, err - } - course.Lessons = lessons - - return &course, nil -} - -// ListCourses lists courses for a tenant with optional filters -func (s *Store) ListCourses(ctx context.Context, tenantID uuid.UUID, filters *CourseFilters) ([]Course, int, error) { - // Count query - countQuery := "SELECT COUNT(*) FROM academy_courses WHERE tenant_id = $1" - countArgs := []interface{}{tenantID} - countArgIdx := 2 - - // List query - query := ` - SELECT - id, tenant_id, title, description, category, - duration_minutes, required_for_roles, is_active, - created_at, updated_at - FROM academy_courses WHERE tenant_id = $1` - - args := []interface{}{tenantID} - argIdx := 2 - - if filters != nil { - if filters.Category != "" { - query += fmt.Sprintf(" AND category = $%d", argIdx) - args = append(args, string(filters.Category)) - argIdx++ - - countQuery += fmt.Sprintf(" AND category = $%d", countArgIdx) - countArgs = append(countArgs, string(filters.Category)) - countArgIdx++ - } - if filters.IsActive != nil { - query += fmt.Sprintf(" AND is_active = $%d", argIdx) - args = append(args, *filters.IsActive) - argIdx++ - - countQuery += fmt.Sprintf(" AND is_active = $%d", countArgIdx) - countArgs = append(countArgs, *filters.IsActive) - countArgIdx++ - } - if filters.Search != "" { - query += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d)", argIdx, argIdx) - args = append(args, "%"+filters.Search+"%") - argIdx++ - - countQuery += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d)", countArgIdx, countArgIdx) - countArgs = append(countArgs, "%"+filters.Search+"%") - countArgIdx++ - } - } - - // Get total count - var total int - err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total) - if err != nil { - return nil, 0, err - } - - query += " ORDER BY created_at DESC" - - if filters != nil && filters.Limit > 0 { - query += fmt.Sprintf(" LIMIT $%d", argIdx) - args = append(args, filters.Limit) - argIdx++ - - if filters.Offset > 0 { - query += fmt.Sprintf(" OFFSET $%d", argIdx) - args = append(args, filters.Offset) - argIdx++ - } - } - - rows, err := s.pool.Query(ctx, query, args...) - if err != nil { - return nil, 0, err - } - defer rows.Close() - - var courses []Course - for rows.Next() { - var course Course - var category string - var requiredForRoles []byte - - err := rows.Scan( - &course.ID, &course.TenantID, &course.Title, &course.Description, &category, - &course.DurationMinutes, &requiredForRoles, &course.IsActive, - &course.CreatedAt, &course.UpdatedAt, - ) - if err != nil { - return nil, 0, err - } - - course.Category = CourseCategory(category) - json.Unmarshal(requiredForRoles, &course.RequiredForRoles) - if course.RequiredForRoles == nil { - course.RequiredForRoles = []string{} - } - - courses = append(courses, course) - } - - if courses == nil { - courses = []Course{} - } - - return courses, total, nil -} - -// UpdateCourse updates a course -func (s *Store) UpdateCourse(ctx context.Context, course *Course) error { - course.UpdatedAt = time.Now().UTC() - - requiredForRoles, _ := json.Marshal(course.RequiredForRoles) - - _, err := s.pool.Exec(ctx, ` - UPDATE academy_courses SET - title = $2, description = $3, category = $4, - duration_minutes = $5, required_for_roles = $6, is_active = $7, - updated_at = $8 - WHERE id = $1 - `, - course.ID, course.Title, course.Description, string(course.Category), - course.DurationMinutes, requiredForRoles, course.IsActive, - course.UpdatedAt, - ) - - return err -} - -// DeleteCourse deletes a course and its related data (via CASCADE) -func (s *Store) DeleteCourse(ctx context.Context, id uuid.UUID) error { - _, err := s.pool.Exec(ctx, "DELETE FROM academy_courses WHERE id = $1", id) - return err -} - -// ============================================================================ -// Lesson Operations -// ============================================================================ - -// CreateLesson creates a new lesson -func (s *Store) CreateLesson(ctx context.Context, lesson *Lesson) error { - lesson.ID = uuid.New() - - quizQuestions, _ := json.Marshal(lesson.QuizQuestions) - - _, err := s.pool.Exec(ctx, ` - INSERT INTO academy_lessons ( - id, course_id, title, description, lesson_type, - content_url, duration_minutes, order_index, quiz_questions - ) VALUES ( - $1, $2, $3, $4, $5, - $6, $7, $8, $9 - ) - `, - lesson.ID, lesson.CourseID, lesson.Title, lesson.Description, string(lesson.LessonType), - lesson.ContentURL, lesson.DurationMinutes, lesson.OrderIndex, quizQuestions, - ) - - return err -} - -// ListLessons lists lessons for a course ordered by order_index -func (s *Store) ListLessons(ctx context.Context, courseID uuid.UUID) ([]Lesson, error) { - rows, err := s.pool.Query(ctx, ` - SELECT - id, course_id, title, description, lesson_type, - content_url, duration_minutes, order_index, quiz_questions - FROM academy_lessons WHERE course_id = $1 - ORDER BY order_index ASC - `, courseID) - if err != nil { - return nil, err - } - defer rows.Close() - - var lessons []Lesson - for rows.Next() { - var lesson Lesson - var lessonType string - var quizQuestions []byte - - err := rows.Scan( - &lesson.ID, &lesson.CourseID, &lesson.Title, &lesson.Description, &lessonType, - &lesson.ContentURL, &lesson.DurationMinutes, &lesson.OrderIndex, &quizQuestions, - ) - if err != nil { - return nil, err - } - - lesson.LessonType = LessonType(lessonType) - json.Unmarshal(quizQuestions, &lesson.QuizQuestions) - if lesson.QuizQuestions == nil { - lesson.QuizQuestions = []QuizQuestion{} - } - - lessons = append(lessons, lesson) - } - - if lessons == nil { - lessons = []Lesson{} - } - - return lessons, nil -} - -// GetLesson retrieves a single lesson by ID -func (s *Store) GetLesson(ctx context.Context, id uuid.UUID) (*Lesson, error) { - var lesson Lesson - var lessonType string - var quizQuestions []byte - - err := s.pool.QueryRow(ctx, ` - SELECT - id, course_id, title, description, lesson_type, - content_url, duration_minutes, order_index, quiz_questions - FROM academy_lessons WHERE id = $1 - `, id).Scan( - &lesson.ID, &lesson.CourseID, &lesson.Title, &lesson.Description, &lessonType, - &lesson.ContentURL, &lesson.DurationMinutes, &lesson.OrderIndex, &quizQuestions, - ) - - if err == pgx.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, err - } - - lesson.LessonType = LessonType(lessonType) - json.Unmarshal(quizQuestions, &lesson.QuizQuestions) - if lesson.QuizQuestions == nil { - lesson.QuizQuestions = []QuizQuestion{} - } - - return &lesson, nil -} - -// UpdateLesson updates a lesson's content, title, and quiz questions -func (s *Store) UpdateLesson(ctx context.Context, lesson *Lesson) error { - quizQuestions, _ := json.Marshal(lesson.QuizQuestions) - - _, err := s.pool.Exec(ctx, ` - UPDATE academy_lessons SET - title = $2, description = $3, content_url = $4, - duration_minutes = $5, quiz_questions = $6 - WHERE id = $1 - `, - lesson.ID, lesson.Title, lesson.Description, - lesson.ContentURL, lesson.DurationMinutes, quizQuestions, - ) - - return err -} - // ============================================================================ // Enrollment Operations // ============================================================================ @@ -519,7 +180,6 @@ func (s *Store) ListEnrollments(ctx context.Context, tenantID uuid.UUID, filters func (s *Store) UpdateEnrollmentProgress(ctx context.Context, id uuid.UUID, progress int, currentLesson int) error { now := time.Now().UTC() - // If progress > 0, set started_at if not already set and update status to in_progress _, err := s.pool.Exec(ctx, ` UPDATE academy_enrollments SET progress_percent = $2, diff --git a/ai-compliance-sdk/internal/api/handlers/roadmap_handlers.go b/ai-compliance-sdk/internal/api/handlers/roadmap_handlers.go index fe1015e..25bb9e5 100644 --- a/ai-compliance-sdk/internal/api/handlers/roadmap_handlers.go +++ b/ai-compliance-sdk/internal/api/handlers/roadmap_handlers.go @@ -1,10 +1,7 @@ package handlers import ( - "bytes" - "io" "net/http" - "time" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/roadmap" @@ -200,541 +197,3 @@ func (h *RoadmapHandlers) GetRoadmapStats(c *gin.Context) { c.JSON(http.StatusOK, stats) } - -// ============================================================================ -// RoadmapItem CRUD -// ============================================================================ - -// CreateItem creates a new roadmap item -// POST /sdk/v1/roadmaps/:id/items -func (h *RoadmapHandlers) CreateItem(c *gin.Context) { - roadmapID, err := uuid.Parse(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid roadmap ID"}) - return - } - - var input roadmap.RoadmapItemInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - item := &roadmap.RoadmapItem{ - RoadmapID: roadmapID, - Title: input.Title, - Description: input.Description, - Category: input.Category, - Priority: input.Priority, - Status: input.Status, - ControlID: input.ControlID, - RegulationRef: input.RegulationRef, - GapID: input.GapID, - EffortDays: input.EffortDays, - AssigneeName: input.AssigneeName, - Department: input.Department, - PlannedStart: input.PlannedStart, - PlannedEnd: input.PlannedEnd, - Notes: input.Notes, - } - - if err := h.store.CreateItem(c.Request.Context(), item); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - // Update roadmap progress - h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID) - - c.JSON(http.StatusCreated, gin.H{"item": item}) -} - -// ListItems lists items for a roadmap -// GET /sdk/v1/roadmaps/:id/items -func (h *RoadmapHandlers) ListItems(c *gin.Context) { - roadmapID, err := uuid.Parse(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid roadmap ID"}) - return - } - - filters := &roadmap.RoadmapItemFilters{ - SearchQuery: c.Query("search"), - Limit: 100, - } - - if status := c.Query("status"); status != "" { - filters.Status = roadmap.ItemStatus(status) - } - if priority := c.Query("priority"); priority != "" { - filters.Priority = roadmap.ItemPriority(priority) - } - if category := c.Query("category"); category != "" { - filters.Category = roadmap.ItemCategory(category) - } - if controlID := c.Query("control_id"); controlID != "" { - filters.ControlID = controlID - } - - items, err := h.store.ListItems(c.Request.Context(), roadmapID, filters) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "items": items, - "total": len(items), - }) -} - -// GetItem retrieves a roadmap item -// GET /sdk/v1/roadmap-items/:id -func (h *RoadmapHandlers) GetItem(c *gin.Context) { - id, err := uuid.Parse(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) - return - } - - item, err := h.store.GetItem(c.Request.Context(), id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if item == nil { - c.JSON(http.StatusNotFound, gin.H{"error": "item not found"}) - return - } - - c.JSON(http.StatusOK, gin.H{"item": item}) -} - -// UpdateItem updates a roadmap item -// PUT /sdk/v1/roadmap-items/:id -func (h *RoadmapHandlers) UpdateItem(c *gin.Context) { - id, err := uuid.Parse(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) - return - } - - item, err := h.store.GetItem(c.Request.Context(), id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if item == nil { - c.JSON(http.StatusNotFound, gin.H{"error": "item not found"}) - return - } - - var input roadmap.RoadmapItemInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Update fields - item.Title = input.Title - item.Description = input.Description - if input.Category != "" { - item.Category = input.Category - } - if input.Priority != "" { - item.Priority = input.Priority - } - if input.Status != "" { - item.Status = input.Status - } - item.ControlID = input.ControlID - item.RegulationRef = input.RegulationRef - item.GapID = input.GapID - item.EffortDays = input.EffortDays - item.AssigneeName = input.AssigneeName - item.Department = input.Department - item.PlannedStart = input.PlannedStart - item.PlannedEnd = input.PlannedEnd - item.Notes = input.Notes - - if err := h.store.UpdateItem(c.Request.Context(), item); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - // Update roadmap progress - h.store.UpdateRoadmapProgress(c.Request.Context(), item.RoadmapID) - - c.JSON(http.StatusOK, gin.H{"item": item}) -} - -// UpdateItemStatus updates just the status of a roadmap item -// PATCH /sdk/v1/roadmap-items/:id/status -func (h *RoadmapHandlers) UpdateItemStatus(c *gin.Context) { - id, err := uuid.Parse(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) - return - } - - var req struct { - Status roadmap.ItemStatus `json:"status"` - } - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - item, err := h.store.GetItem(c.Request.Context(), id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if item == nil { - c.JSON(http.StatusNotFound, gin.H{"error": "item not found"}) - return - } - - item.Status = req.Status - - // Set actual dates - now := time.Now().UTC() - if req.Status == roadmap.ItemStatusInProgress && item.ActualStart == nil { - item.ActualStart = &now - } - if req.Status == roadmap.ItemStatusCompleted && item.ActualEnd == nil { - item.ActualEnd = &now - } - - if err := h.store.UpdateItem(c.Request.Context(), item); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - // Update roadmap progress - h.store.UpdateRoadmapProgress(c.Request.Context(), item.RoadmapID) - - c.JSON(http.StatusOK, gin.H{"item": item}) -} - -// DeleteItem deletes a roadmap item -// DELETE /sdk/v1/roadmap-items/:id -func (h *RoadmapHandlers) DeleteItem(c *gin.Context) { - id, err := uuid.Parse(c.Param("id")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) - return - } - - item, err := h.store.GetItem(c.Request.Context(), id) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if item == nil { - c.JSON(http.StatusNotFound, gin.H{"error": "item not found"}) - return - } - - roadmapID := item.RoadmapID - - if err := h.store.DeleteItem(c.Request.Context(), id); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - // Update roadmap progress - h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID) - - c.JSON(http.StatusOK, gin.H{"message": "item deleted"}) -} - -// ============================================================================ -// Import Workflow -// ============================================================================ - -// UploadImport handles file upload for import -// POST /sdk/v1/roadmaps/import/upload -func (h *RoadmapHandlers) UploadImport(c *gin.Context) { - tenantID := rbac.GetTenantID(c) - userID := rbac.GetUserID(c) - - // Get file from form - file, header, err := c.Request.FormFile("file") - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"}) - return - } - defer file.Close() - - // Read file content - buf := bytes.Buffer{} - if _, err := io.Copy(&buf, file); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"}) - return - } - - // Detect format - format := roadmap.ImportFormat("") - filename := header.Filename - contentType := header.Header.Get("Content-Type") - - // Create import job - job := &roadmap.ImportJob{ - TenantID: tenantID, - Filename: filename, - FileSize: header.Size, - ContentType: contentType, - Status: "pending", - CreatedBy: userID, - } - - if err := h.store.CreateImportJob(c.Request.Context(), job); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - // Parse the file - job.Status = "parsing" - h.store.UpdateImportJob(c.Request.Context(), job) - - result, err := h.parser.ParseFile(buf.Bytes(), filename, contentType) - if err != nil { - job.Status = "failed" - job.ErrorMessage = err.Error() - h.store.UpdateImportJob(c.Request.Context(), job) - - c.JSON(http.StatusBadRequest, gin.H{ - "error": "failed to parse file", - "detail": err.Error(), - }) - return - } - - // Update job with parsed data - job.Status = "parsed" - job.Format = format - job.TotalRows = result.TotalRows - job.ValidRows = result.ValidRows - job.InvalidRows = result.InvalidRows - job.ParsedItems = result.Items - - h.store.UpdateImportJob(c.Request.Context(), job) - - c.JSON(http.StatusOK, roadmap.ImportParseResponse{ - JobID: job.ID, - Status: job.Status, - TotalRows: result.TotalRows, - ValidRows: result.ValidRows, - InvalidRows: result.InvalidRows, - Items: result.Items, - ColumnMap: buildColumnMap(result.Columns), - }) -} - -// GetImportJob returns the status of an import job -// GET /sdk/v1/roadmaps/import/:jobId -func (h *RoadmapHandlers) GetImportJob(c *gin.Context) { - jobID, err := uuid.Parse(c.Param("jobId")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job ID"}) - return - } - - job, err := h.store.GetImportJob(c.Request.Context(), jobID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if job == nil { - c.JSON(http.StatusNotFound, gin.H{"error": "import job not found"}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "job": job, - "items": job.ParsedItems, - }) -} - -// ConfirmImport confirms and executes the import -// POST /sdk/v1/roadmaps/import/:jobId/confirm -func (h *RoadmapHandlers) ConfirmImport(c *gin.Context) { - jobID, err := uuid.Parse(c.Param("jobId")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job ID"}) - return - } - - var req roadmap.ImportConfirmRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - job, err := h.store.GetImportJob(c.Request.Context(), jobID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if job == nil { - c.JSON(http.StatusNotFound, gin.H{"error": "import job not found"}) - return - } - - if job.Status != "parsed" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "job is not ready for confirmation", - "status": job.Status, - }) - return - } - - tenantID := rbac.GetTenantID(c) - userID := rbac.GetUserID(c) - - // Create or use existing roadmap - var roadmapID uuid.UUID - if req.RoadmapID != nil { - roadmapID = *req.RoadmapID - // Verify roadmap exists - r, err := h.store.GetRoadmap(c.Request.Context(), roadmapID) - if err != nil || r == nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "roadmap not found"}) - return - } - } else { - // Create new roadmap - title := req.RoadmapTitle - if title == "" { - title = "Imported Roadmap - " + job.Filename - } - - r := &roadmap.Roadmap{ - TenantID: tenantID, - Title: title, - Status: "active", - CreatedBy: userID, - } - - if err := h.store.CreateRoadmap(c.Request.Context(), r); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - roadmapID = r.ID - } - - // Determine which rows to import - selectedRows := make(map[int]bool) - if len(req.SelectedRows) > 0 { - for _, row := range req.SelectedRows { - selectedRows[row] = true - } - } - - // Convert parsed items to roadmap items - var items []roadmap.RoadmapItem - var importedCount, skippedCount int - - for i, parsed := range job.ParsedItems { - // Skip invalid items - if !parsed.IsValid { - skippedCount++ - continue - } - - // Skip unselected rows if selection was specified - if len(selectedRows) > 0 && !selectedRows[parsed.RowNumber] { - skippedCount++ - continue - } - - item := roadmap.RoadmapItem{ - RoadmapID: roadmapID, - Title: parsed.Data.Title, - Description: parsed.Data.Description, - Category: parsed.Data.Category, - Priority: parsed.Data.Priority, - Status: parsed.Data.Status, - ControlID: parsed.Data.ControlID, - RegulationRef: parsed.Data.RegulationRef, - GapID: parsed.Data.GapID, - EffortDays: parsed.Data.EffortDays, - AssigneeName: parsed.Data.AssigneeName, - Department: parsed.Data.Department, - PlannedStart: parsed.Data.PlannedStart, - PlannedEnd: parsed.Data.PlannedEnd, - Notes: parsed.Data.Notes, - SourceRow: parsed.RowNumber, - SourceFile: job.Filename, - SortOrder: i, - } - - // Apply auto-mappings if requested - if req.ApplyMappings { - if parsed.MatchedControl != "" { - item.ControlID = parsed.MatchedControl - } - if parsed.MatchedRegulation != "" { - item.RegulationRef = parsed.MatchedRegulation - } - if parsed.MatchedGap != "" { - item.GapID = parsed.MatchedGap - } - } - - // Set defaults - if item.Status == "" { - item.Status = roadmap.ItemStatusPlanned - } - if item.Priority == "" { - item.Priority = roadmap.ItemPriorityMedium - } - if item.Category == "" { - item.Category = roadmap.ItemCategoryTechnical - } - - items = append(items, item) - importedCount++ - } - - // Bulk create items - if len(items) > 0 { - if err := h.store.BulkCreateItems(c.Request.Context(), items); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - } - - // Update roadmap progress - h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID) - - // Update job status - now := time.Now().UTC() - job.Status = "completed" - job.RoadmapID = &roadmapID - job.ImportedItems = importedCount - job.CompletedAt = &now - h.store.UpdateImportJob(c.Request.Context(), job) - - c.JSON(http.StatusOK, roadmap.ImportConfirmResponse{ - RoadmapID: roadmapID, - ImportedItems: importedCount, - SkippedItems: skippedCount, - Message: "Import completed successfully", - }) -} - -// ============================================================================ -// Helper Functions -// ============================================================================ - -func buildColumnMap(columns []roadmap.DetectedColumn) map[string]string { - result := make(map[string]string) - for _, col := range columns { - if col.MappedTo != "" { - result[col.Header] = col.MappedTo - } - } - return result -} diff --git a/ai-compliance-sdk/internal/api/handlers/roadmap_import_handlers.go b/ai-compliance-sdk/internal/api/handlers/roadmap_import_handlers.go new file mode 100644 index 0000000..21ce287 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/roadmap_import_handlers.go @@ -0,0 +1,303 @@ +package handlers + +import ( + "bytes" + "io" + "net/http" + "time" + + "github.com/breakpilot/ai-compliance-sdk/internal/rbac" + "github.com/breakpilot/ai-compliance-sdk/internal/roadmap" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// ============================================================================ +// Import Workflow +// ============================================================================ + +// UploadImport handles file upload for import +// POST /sdk/v1/roadmaps/import/upload +func (h *RoadmapHandlers) UploadImport(c *gin.Context) { + tenantID := rbac.GetTenantID(c) + userID := rbac.GetUserID(c) + + // Get file from form + file, header, err := c.Request.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"}) + return + } + defer file.Close() + + // Read file content + buf := bytes.Buffer{} + if _, err := io.Copy(&buf, file); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"}) + return + } + + // Detect format + format := roadmap.ImportFormat("") + filename := header.Filename + contentType := header.Header.Get("Content-Type") + + // Create import job + job := &roadmap.ImportJob{ + TenantID: tenantID, + Filename: filename, + FileSize: header.Size, + ContentType: contentType, + Status: "pending", + CreatedBy: userID, + } + + if err := h.store.CreateImportJob(c.Request.Context(), job); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Parse the file + job.Status = "parsing" + h.store.UpdateImportJob(c.Request.Context(), job) + + result, err := h.parser.ParseFile(buf.Bytes(), filename, contentType) + if err != nil { + job.Status = "failed" + job.ErrorMessage = err.Error() + h.store.UpdateImportJob(c.Request.Context(), job) + + c.JSON(http.StatusBadRequest, gin.H{ + "error": "failed to parse file", + "detail": err.Error(), + }) + return + } + + // Update job with parsed data + job.Status = "parsed" + job.Format = format + job.TotalRows = result.TotalRows + job.ValidRows = result.ValidRows + job.InvalidRows = result.InvalidRows + job.ParsedItems = result.Items + + h.store.UpdateImportJob(c.Request.Context(), job) + + c.JSON(http.StatusOK, roadmap.ImportParseResponse{ + JobID: job.ID, + Status: job.Status, + TotalRows: result.TotalRows, + ValidRows: result.ValidRows, + InvalidRows: result.InvalidRows, + Items: result.Items, + ColumnMap: buildColumnMap(result.Columns), + }) +} + +// GetImportJob returns the status of an import job +// GET /sdk/v1/roadmaps/import/:jobId +func (h *RoadmapHandlers) GetImportJob(c *gin.Context) { + jobID, err := uuid.Parse(c.Param("jobId")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job ID"}) + return + } + + job, err := h.store.GetImportJob(c.Request.Context(), jobID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if job == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "import job not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "job": job, + "items": job.ParsedItems, + }) +} + +// ConfirmImport confirms and executes the import +// POST /sdk/v1/roadmaps/import/:jobId/confirm +func (h *RoadmapHandlers) ConfirmImport(c *gin.Context) { + jobID, err := uuid.Parse(c.Param("jobId")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job ID"}) + return + } + + var req roadmap.ImportConfirmRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + job, err := h.store.GetImportJob(c.Request.Context(), jobID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if job == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "import job not found"}) + return + } + + if job.Status != "parsed" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "job is not ready for confirmation", + "status": job.Status, + }) + return + } + + tenantID := rbac.GetTenantID(c) + userID := rbac.GetUserID(c) + + // Create or use existing roadmap + var roadmapID uuid.UUID + if req.RoadmapID != nil { + roadmapID = *req.RoadmapID + // Verify roadmap exists + r, err := h.store.GetRoadmap(c.Request.Context(), roadmapID) + if err != nil || r == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "roadmap not found"}) + return + } + } else { + // Create new roadmap + title := req.RoadmapTitle + if title == "" { + title = "Imported Roadmap - " + job.Filename + } + + r := &roadmap.Roadmap{ + TenantID: tenantID, + Title: title, + Status: "active", + CreatedBy: userID, + } + + if err := h.store.CreateRoadmap(c.Request.Context(), r); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + roadmapID = r.ID + } + + // Determine which rows to import + selectedRows := make(map[int]bool) + if len(req.SelectedRows) > 0 { + for _, row := range req.SelectedRows { + selectedRows[row] = true + } + } + + // Convert parsed items to roadmap items + var items []roadmap.RoadmapItem + var importedCount, skippedCount int + + for i, parsed := range job.ParsedItems { + // Skip invalid items + if !parsed.IsValid { + skippedCount++ + continue + } + + // Skip unselected rows if selection was specified + if len(selectedRows) > 0 && !selectedRows[parsed.RowNumber] { + skippedCount++ + continue + } + + item := roadmap.RoadmapItem{ + RoadmapID: roadmapID, + Title: parsed.Data.Title, + Description: parsed.Data.Description, + Category: parsed.Data.Category, + Priority: parsed.Data.Priority, + Status: parsed.Data.Status, + ControlID: parsed.Data.ControlID, + RegulationRef: parsed.Data.RegulationRef, + GapID: parsed.Data.GapID, + EffortDays: parsed.Data.EffortDays, + AssigneeName: parsed.Data.AssigneeName, + Department: parsed.Data.Department, + PlannedStart: parsed.Data.PlannedStart, + PlannedEnd: parsed.Data.PlannedEnd, + Notes: parsed.Data.Notes, + SourceRow: parsed.RowNumber, + SourceFile: job.Filename, + SortOrder: i, + } + + // Apply auto-mappings if requested + if req.ApplyMappings { + if parsed.MatchedControl != "" { + item.ControlID = parsed.MatchedControl + } + if parsed.MatchedRegulation != "" { + item.RegulationRef = parsed.MatchedRegulation + } + if parsed.MatchedGap != "" { + item.GapID = parsed.MatchedGap + } + } + + // Set defaults + if item.Status == "" { + item.Status = roadmap.ItemStatusPlanned + } + if item.Priority == "" { + item.Priority = roadmap.ItemPriorityMedium + } + if item.Category == "" { + item.Category = roadmap.ItemCategoryTechnical + } + + items = append(items, item) + importedCount++ + } + + // Bulk create items + if len(items) > 0 { + if err := h.store.BulkCreateItems(c.Request.Context(), items); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } + + // Update roadmap progress + h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID) + + // Update job status + now := time.Now().UTC() + job.Status = "completed" + job.RoadmapID = &roadmapID + job.ImportedItems = importedCount + job.CompletedAt = &now + h.store.UpdateImportJob(c.Request.Context(), job) + + c.JSON(http.StatusOK, roadmap.ImportConfirmResponse{ + RoadmapID: roadmapID, + ImportedItems: importedCount, + SkippedItems: skippedCount, + Message: "Import completed successfully", + }) +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +func buildColumnMap(columns []roadmap.DetectedColumn) map[string]string { + result := make(map[string]string) + for _, col := range columns { + if col.MappedTo != "" { + result[col.Header] = col.MappedTo + } + } + return result +} diff --git a/ai-compliance-sdk/internal/api/handlers/roadmap_item_handlers.go b/ai-compliance-sdk/internal/api/handlers/roadmap_item_handlers.go new file mode 100644 index 0000000..e562cfd --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/roadmap_item_handlers.go @@ -0,0 +1,258 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/breakpilot/ai-compliance-sdk/internal/roadmap" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// ============================================================================ +// RoadmapItem CRUD +// ============================================================================ + +// CreateItem creates a new roadmap item +// POST /sdk/v1/roadmaps/:id/items +func (h *RoadmapHandlers) CreateItem(c *gin.Context) { + roadmapID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid roadmap ID"}) + return + } + + var input roadmap.RoadmapItemInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + item := &roadmap.RoadmapItem{ + RoadmapID: roadmapID, + Title: input.Title, + Description: input.Description, + Category: input.Category, + Priority: input.Priority, + Status: input.Status, + ControlID: input.ControlID, + RegulationRef: input.RegulationRef, + GapID: input.GapID, + EffortDays: input.EffortDays, + AssigneeName: input.AssigneeName, + Department: input.Department, + PlannedStart: input.PlannedStart, + PlannedEnd: input.PlannedEnd, + Notes: input.Notes, + } + + if err := h.store.CreateItem(c.Request.Context(), item); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Update roadmap progress + h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID) + + c.JSON(http.StatusCreated, gin.H{"item": item}) +} + +// ListItems lists items for a roadmap +// GET /sdk/v1/roadmaps/:id/items +func (h *RoadmapHandlers) ListItems(c *gin.Context) { + roadmapID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid roadmap ID"}) + return + } + + filters := &roadmap.RoadmapItemFilters{ + SearchQuery: c.Query("search"), + Limit: 100, + } + + if status := c.Query("status"); status != "" { + filters.Status = roadmap.ItemStatus(status) + } + if priority := c.Query("priority"); priority != "" { + filters.Priority = roadmap.ItemPriority(priority) + } + if category := c.Query("category"); category != "" { + filters.Category = roadmap.ItemCategory(category) + } + if controlID := c.Query("control_id"); controlID != "" { + filters.ControlID = controlID + } + + items, err := h.store.ListItems(c.Request.Context(), roadmapID, filters) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "items": items, + "total": len(items), + }) +} + +// GetItem retrieves a roadmap item +// GET /sdk/v1/roadmap-items/:id +func (h *RoadmapHandlers) GetItem(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) + return + } + + item, err := h.store.GetItem(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if item == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "item not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{"item": item}) +} + +// UpdateItem updates a roadmap item +// PUT /sdk/v1/roadmap-items/:id +func (h *RoadmapHandlers) UpdateItem(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) + return + } + + item, err := h.store.GetItem(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if item == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "item not found"}) + return + } + + var input roadmap.RoadmapItemInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Update fields + item.Title = input.Title + item.Description = input.Description + if input.Category != "" { + item.Category = input.Category + } + if input.Priority != "" { + item.Priority = input.Priority + } + if input.Status != "" { + item.Status = input.Status + } + item.ControlID = input.ControlID + item.RegulationRef = input.RegulationRef + item.GapID = input.GapID + item.EffortDays = input.EffortDays + item.AssigneeName = input.AssigneeName + item.Department = input.Department + item.PlannedStart = input.PlannedStart + item.PlannedEnd = input.PlannedEnd + item.Notes = input.Notes + + if err := h.store.UpdateItem(c.Request.Context(), item); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Update roadmap progress + h.store.UpdateRoadmapProgress(c.Request.Context(), item.RoadmapID) + + c.JSON(http.StatusOK, gin.H{"item": item}) +} + +// UpdateItemStatus updates just the status of a roadmap item +// PATCH /sdk/v1/roadmap-items/:id/status +func (h *RoadmapHandlers) UpdateItemStatus(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) + return + } + + var req struct { + Status roadmap.ItemStatus `json:"status"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + item, err := h.store.GetItem(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if item == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "item not found"}) + return + } + + item.Status = req.Status + + // Set actual dates + now := time.Now().UTC() + if req.Status == roadmap.ItemStatusInProgress && item.ActualStart == nil { + item.ActualStart = &now + } + if req.Status == roadmap.ItemStatusCompleted && item.ActualEnd == nil { + item.ActualEnd = &now + } + + if err := h.store.UpdateItem(c.Request.Context(), item); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Update roadmap progress + h.store.UpdateRoadmapProgress(c.Request.Context(), item.RoadmapID) + + c.JSON(http.StatusOK, gin.H{"item": item}) +} + +// DeleteItem deletes a roadmap item +// DELETE /sdk/v1/roadmap-items/:id +func (h *RoadmapHandlers) DeleteItem(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) + return + } + + item, err := h.store.GetItem(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if item == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "item not found"}) + return + } + + roadmapID := item.RoadmapID + + if err := h.store.DeleteItem(c.Request.Context(), id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Update roadmap progress + h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID) + + c.JSON(http.StatusOK, gin.H{"message": "item deleted"}) +} diff --git a/ai-compliance-sdk/internal/app/app.go b/ai-compliance-sdk/internal/app/app.go new file mode 100644 index 0000000..bf27feb --- /dev/null +++ b/ai-compliance-sdk/internal/app/app.go @@ -0,0 +1,161 @@ +package app + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/breakpilot/ai-compliance-sdk/internal/academy" + "github.com/breakpilot/ai-compliance-sdk/internal/api/handlers" + "github.com/breakpilot/ai-compliance-sdk/internal/audit" + "github.com/breakpilot/ai-compliance-sdk/internal/config" + "github.com/breakpilot/ai-compliance-sdk/internal/iace" + "github.com/breakpilot/ai-compliance-sdk/internal/llm" + "github.com/breakpilot/ai-compliance-sdk/internal/portfolio" + "github.com/breakpilot/ai-compliance-sdk/internal/rbac" + "github.com/breakpilot/ai-compliance-sdk/internal/roadmap" + "github.com/breakpilot/ai-compliance-sdk/internal/training" + "github.com/breakpilot/ai-compliance-sdk/internal/ucca" + "github.com/breakpilot/ai-compliance-sdk/internal/whistleblower" + "github.com/breakpilot/ai-compliance-sdk/internal/workshop" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgxpool" +) + +// Run initializes and starts the AI Compliance SDK server. +func Run() { + cfg, err := config.Load() + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + if cfg.IsProduction() { + gin.SetMode(gin.ReleaseMode) + } + + ctx := context.Background() + pool, err := pgxpool.New(ctx, cfg.DatabaseURL) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + defer pool.Close() + + if err := pool.Ping(ctx); err != nil { + log.Fatalf("Failed to ping database: %v", err) + } + log.Println("Connected to database") + + router := buildRouter(cfg, pool) + srv := &http.Server{ + Addr: ":" + cfg.Port, + Handler: router, + ReadTimeout: 30 * time.Second, + WriteTimeout: 5 * time.Minute, + IdleTimeout: 60 * time.Second, + } + + go func() { + log.Printf("AI Compliance SDK starting on port %s", cfg.Port) + log.Printf("Environment: %s", cfg.Environment) + log.Printf("Primary LLM Provider: %s", cfg.LLMProvider) + log.Printf("Fallback LLM Provider: %s", cfg.LLMFallbackProvider) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server failed: %v", err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + log.Println("Server exited") +} + +// buildRouter wires all stores, services, and handlers onto a new Gin engine. +func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine { + // Stores + rbacStore := rbac.NewStore(pool) + auditStore := audit.NewStore(pool) + uccaStore := ucca.NewStore(pool) + escalationStore := ucca.NewEscalationStore(pool) + corpusVersionStore := ucca.NewCorpusVersionStore(pool) + roadmapStore := roadmap.NewStore(pool) + workshopStore := workshop.NewStore(pool) + portfolioStore := portfolio.NewStore(pool) + academyStore := academy.NewStore(pool) + whistleblowerStore := whistleblower.NewStore(pool) + iaceStore := iace.NewStore(pool) + trainingStore := training.NewStore(pool) + obligationsStore := ucca.NewObligationsStore(pool) + + // Services + rbacService := rbac.NewService(rbacStore) + policyEngine := rbac.NewPolicyEngine(rbacService, rbacStore) + + // LLM providers + providerRegistry := llm.NewProviderRegistry(cfg.LLMProvider, cfg.LLMFallbackProvider) + providerRegistry.Register(llm.NewOllamaAdapter(cfg.OllamaURL, cfg.OllamaDefaultModel)) + if cfg.AnthropicAPIKey != "" { + providerRegistry.Register(llm.NewAnthropicAdapter(cfg.AnthropicAPIKey, cfg.AnthropicDefaultModel)) + } + + piiDetector := llm.NewPIIDetectorWithPatterns(llm.AllPIIPatterns()) + ttsClient := training.NewTTSClient(cfg.TTSServiceURL) + contentGenerator := training.NewContentGenerator(providerRegistry, piiDetector, trainingStore, ttsClient) + accessGate := llm.NewAccessGate(policyEngine, piiDetector, providerRegistry) + trailBuilder := audit.NewTrailBuilder(auditStore) + exporter := audit.NewExporter(auditStore) + blockGenerator := training.NewBlockGenerator(trainingStore, contentGenerator) + + // Handlers + rbacHandlers := handlers.NewRBACHandlers(rbacStore, rbacService, policyEngine) + llmHandlers := handlers.NewLLMHandlers(accessGate, providerRegistry, piiDetector, auditStore, trailBuilder) + auditHandlers := handlers.NewAuditHandlers(auditStore, exporter) + uccaHandlers := handlers.NewUCCAHandlers(uccaStore, escalationStore, providerRegistry) + escalationHandlers := handlers.NewEscalationHandlers(escalationStore, uccaStore) + roadmapHandlers := handlers.NewRoadmapHandlers(roadmapStore) + workshopHandlers := handlers.NewWorkshopHandlers(workshopStore) + portfolioHandlers := handlers.NewPortfolioHandlers(portfolioStore) + academyHandlers := handlers.NewAcademyHandlers(academyStore, trainingStore) + whistleblowerHandlers := handlers.NewWhistleblowerHandlers(whistleblowerStore) + iaceHandler := handlers.NewIACEHandler(iaceStore, providerRegistry) + trainingHandlers := handlers.NewTrainingHandlers(trainingStore, contentGenerator, blockGenerator, ttsClient) + ragHandlers := handlers.NewRAGHandlers(corpusVersionStore) + obligationsHandlers := handlers.NewObligationsHandlersWithStore(obligationsStore) + rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine) + + // Router + router := gin.Default() + router.Use(cors.New(cors.Config{ + AllowOrigins: cfg.AllowedOrigins, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "X-User-ID", "X-Tenant-ID", "X-Namespace-ID", "X-Tenant-Slug"}, + ExposeHeaders: []string{"Content-Length", "Content-Disposition"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + router.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "healthy", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }) + }) + + registerRoutes(router, rbacMiddleware, + rbacHandlers, llmHandlers, auditHandlers, + uccaHandlers, escalationHandlers, obligationsHandlers, ragHandlers, + roadmapHandlers, workshopHandlers, portfolioHandlers, + academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler) + + return router +} diff --git a/ai-compliance-sdk/internal/app/routes.go b/ai-compliance-sdk/internal/app/routes.go new file mode 100644 index 0000000..553a049 --- /dev/null +++ b/ai-compliance-sdk/internal/app/routes.go @@ -0,0 +1,409 @@ +package app + +import ( + "net/http" + "time" + + "github.com/breakpilot/ai-compliance-sdk/internal/api/handlers" + "github.com/breakpilot/ai-compliance-sdk/internal/rbac" + "github.com/gin-gonic/gin" +) + +func registerRoutes( + router *gin.Engine, + rbacMiddleware *rbac.Middleware, + rbacHandlers *handlers.RBACHandlers, + llmHandlers *handlers.LLMHandlers, + auditHandlers *handlers.AuditHandlers, + uccaHandlers *handlers.UCCAHandlers, + escalationHandlers *handlers.EscalationHandlers, + obligationsHandlers *handlers.ObligationsHandlers, + ragHandlers *handlers.RAGHandlers, + roadmapHandlers *handlers.RoadmapHandlers, + workshopHandlers *handlers.WorkshopHandlers, + portfolioHandlers *handlers.PortfolioHandlers, + academyHandlers *handlers.AcademyHandlers, + trainingHandlers *handlers.TrainingHandlers, + whistleblowerHandlers *handlers.WhistleblowerHandlers, + iaceHandler *handlers.IACEHandler, +) { + v1 := router.Group("/sdk/v1") + { + v1.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok", "timestamp": time.Now().UTC()}) + }) + v1.Use(rbacMiddleware.ExtractUserContext()) + + registerRBACRoutes(v1, rbacHandlers) + registerLLMRoutes(v1, rbacMiddleware, llmHandlers) + registerAuditRoutes(v1, rbacMiddleware, auditHandlers) + registerUCCARoutes(v1, uccaHandlers, escalationHandlers, obligationsHandlers) + registerRAGRoutes(v1, ragHandlers) + registerRoadmapRoutes(v1, roadmapHandlers) + registerWorkshopRoutes(v1, workshopHandlers) + registerPortfolioRoutes(v1, portfolioHandlers) + registerAcademyRoutes(v1, academyHandlers) + registerTrainingRoutes(v1, trainingHandlers) + registerWhistleblowerRoutes(v1, whistleblowerHandlers) + registerIACERoutes(v1, iaceHandler) + } +} + +func registerRBACRoutes(v1 *gin.RouterGroup, h *handlers.RBACHandlers) { + tenants := v1.Group("/tenants") + { + tenants.GET("", h.ListTenants) + tenants.GET("/:id", h.GetTenant) + tenants.POST("", h.CreateTenant) + tenants.PUT("/:id", h.UpdateTenant) + tenants.GET("/:id/namespaces", h.ListNamespaces) + tenants.POST("/:id/namespaces", h.CreateNamespace) + } + v1.GET("/namespaces/:id", h.GetNamespace) + roles := v1.Group("/roles") + { + roles.GET("", h.ListRoles) + roles.GET("/system", h.ListSystemRoles) + roles.GET("/:id", h.GetRole) + roles.POST("", h.CreateRole) + } + userRoles := v1.Group("/user-roles") + { + userRoles.POST("", h.AssignRole) + userRoles.DELETE("/:userId/:roleId", h.RevokeRole) + userRoles.GET("/:userId", h.GetUserRoles) + } + permissions := v1.Group("/permissions") + { + permissions.GET("/effective", h.GetEffectivePermissions) + permissions.GET("/context", h.GetUserContext) + permissions.GET("/check", h.CheckPermission) + } + policies := v1.Group("/llm/policies") + { + policies.GET("", h.ListLLMPolicies) + policies.GET("/:id", h.GetLLMPolicy) + policies.POST("", h.CreateLLMPolicy) + policies.PUT("/:id", h.UpdateLLMPolicy) + policies.DELETE("/:id", h.DeleteLLMPolicy) + } +} + +func registerLLMRoutes(v1 *gin.RouterGroup, mw *rbac.Middleware, h *handlers.LLMHandlers) { + llmRoutes := v1.Group("/llm") + llmRoutes.Use(mw.RequireLLMAccess()) + { + llmRoutes.POST("/chat", h.Chat) + llmRoutes.POST("/complete", h.Complete) + llmRoutes.GET("/models", h.ListModels) + llmRoutes.GET("/providers/status", h.GetProviderStatus) + llmRoutes.POST("/analyze", h.AnalyzeText) + llmRoutes.POST("/redact", h.RedactText) + } +} + +func registerAuditRoutes(v1 *gin.RouterGroup, mw *rbac.Middleware, h *handlers.AuditHandlers) { + auditRoutes := v1.Group("/audit") + auditRoutes.Use(mw.RequireAnyPermission(rbac.PermissionAuditAll, rbac.PermissionAuditRead, rbac.PermissionAuditLogRead)) + { + auditRoutes.GET("/llm", h.QueryLLMAudit) + auditRoutes.GET("/general", h.QueryGeneralAudit) + auditRoutes.GET("/llm-operations", h.QueryLLMAudit) + auditRoutes.GET("/trail", h.QueryGeneralAudit) + auditRoutes.GET("/usage", h.GetUsageStats) + auditRoutes.GET("/compliance-report", h.GetComplianceReport) + auditRoutes.GET("/export/llm", h.ExportLLMAudit) + auditRoutes.GET("/export/general", h.ExportGeneralAudit) + auditRoutes.GET("/export/compliance", h.ExportComplianceReport) + } +} + +func registerUCCARoutes(v1 *gin.RouterGroup, h *handlers.UCCAHandlers, eh *handlers.EscalationHandlers, oh *handlers.ObligationsHandlers) { + uccaRoutes := v1.Group("/ucca") + { + uccaRoutes.POST("/assess", h.Assess) + uccaRoutes.GET("/assessments", h.ListAssessments) + uccaRoutes.GET("/assessments/:id", h.GetAssessment) + uccaRoutes.PUT("/assessments/:id", h.UpdateAssessment) + uccaRoutes.DELETE("/assessments/:id", h.DeleteAssessment) + uccaRoutes.POST("/assessments/:id/explain", h.Explain) + uccaRoutes.GET("/patterns", h.ListPatterns) + uccaRoutes.GET("/examples", h.ListExamples) + uccaRoutes.GET("/rules", h.ListRules) + uccaRoutes.GET("/controls", h.ListControls) + uccaRoutes.GET("/problem-solutions", h.ListProblemSolutions) + uccaRoutes.GET("/export/:id", h.Export) + uccaRoutes.GET("/escalations", eh.ListEscalations) + uccaRoutes.GET("/escalations/stats", eh.GetEscalationStats) + uccaRoutes.GET("/escalations/:id", eh.GetEscalation) + uccaRoutes.POST("/escalations", eh.CreateEscalation) + uccaRoutes.POST("/escalations/:id/assign", eh.AssignEscalation) + uccaRoutes.POST("/escalations/:id/review", eh.StartReview) + uccaRoutes.POST("/escalations/:id/decide", eh.DecideEscalation) + oh.RegisterRoutes(uccaRoutes) + } +} + +func registerRAGRoutes(v1 *gin.RouterGroup, h *handlers.RAGHandlers) { + ragRoutes := v1.Group("/rag") + { + ragRoutes.POST("/search", h.Search) + ragRoutes.GET("/regulations", h.ListRegulations) + ragRoutes.GET("/corpus-status", h.CorpusStatus) + ragRoutes.GET("/corpus-versions/:collection", h.CorpusVersionHistory) + ragRoutes.GET("/scroll", h.HandleScrollChunks) + } +} + +func registerRoadmapRoutes(v1 *gin.RouterGroup, h *handlers.RoadmapHandlers) { + roadmapRoutes := v1.Group("/roadmaps") + { + roadmapRoutes.POST("", h.CreateRoadmap) + roadmapRoutes.GET("", h.ListRoadmaps) + roadmapRoutes.GET("/:id", h.GetRoadmap) + roadmapRoutes.PUT("/:id", h.UpdateRoadmap) + roadmapRoutes.DELETE("/:id", h.DeleteRoadmap) + roadmapRoutes.GET("/:id/stats", h.GetRoadmapStats) + roadmapRoutes.POST("/:id/items", h.CreateItem) + roadmapRoutes.GET("/:id/items", h.ListItems) + roadmapRoutes.POST("/import/upload", h.UploadImport) + roadmapRoutes.GET("/import/:jobId", h.GetImportJob) + roadmapRoutes.POST("/import/:jobId/confirm", h.ConfirmImport) + } + roadmapItemRoutes := v1.Group("/roadmap-items") + { + roadmapItemRoutes.GET("/:id", h.GetItem) + roadmapItemRoutes.PUT("/:id", h.UpdateItem) + roadmapItemRoutes.PATCH("/:id/status", h.UpdateItemStatus) + roadmapItemRoutes.DELETE("/:id", h.DeleteItem) + } +} + +func registerWorkshopRoutes(v1 *gin.RouterGroup, h *handlers.WorkshopHandlers) { + workshopRoutes := v1.Group("/workshops") + { + workshopRoutes.POST("", h.CreateSession) + workshopRoutes.GET("", h.ListSessions) + workshopRoutes.GET("/:id", h.GetSession) + workshopRoutes.PUT("/:id", h.UpdateSession) + workshopRoutes.DELETE("/:id", h.DeleteSession) + workshopRoutes.POST("/:id/start", h.StartSession) + workshopRoutes.POST("/:id/pause", h.PauseSession) + workshopRoutes.POST("/:id/complete", h.CompleteSession) + workshopRoutes.GET("/:id/participants", h.ListParticipants) + workshopRoutes.PUT("/:id/participants/:participantId", h.UpdateParticipant) + workshopRoutes.DELETE("/:id/participants/:participantId", h.RemoveParticipant) + workshopRoutes.POST("/:id/responses", h.SubmitResponse) + workshopRoutes.GET("/:id/responses", h.GetResponses) + workshopRoutes.POST("/:id/comments", h.AddComment) + workshopRoutes.GET("/:id/comments", h.GetComments) + workshopRoutes.POST("/:id/advance", h.AdvanceStep) + workshopRoutes.POST("/:id/goto", h.GoToStep) + workshopRoutes.GET("/:id/stats", h.GetSessionStats) + workshopRoutes.GET("/:id/summary", h.GetSessionSummary) + workshopRoutes.GET("/:id/export", h.ExportSession) + workshopRoutes.POST("/join/:code", h.JoinSession) + } +} + +func registerPortfolioRoutes(v1 *gin.RouterGroup, h *handlers.PortfolioHandlers) { + portfolioRoutes := v1.Group("/portfolios") + { + portfolioRoutes.POST("", h.CreatePortfolio) + portfolioRoutes.GET("", h.ListPortfolios) + portfolioRoutes.GET("/:id", h.GetPortfolio) + portfolioRoutes.PUT("/:id", h.UpdatePortfolio) + portfolioRoutes.DELETE("/:id", h.DeletePortfolio) + portfolioRoutes.POST("/:id/items", h.AddItem) + portfolioRoutes.GET("/:id/items", h.ListItems) + portfolioRoutes.POST("/:id/items/bulk", h.BulkAddItems) + portfolioRoutes.DELETE("/:id/items/:itemId", h.RemoveItem) + portfolioRoutes.PUT("/:id/items/order", h.ReorderItems) + portfolioRoutes.GET("/:id/stats", h.GetPortfolioStats) + portfolioRoutes.GET("/:id/activity", h.GetPortfolioActivity) + portfolioRoutes.POST("/:id/recalculate", h.RecalculateMetrics) + portfolioRoutes.POST("/:id/submit-review", h.SubmitForReview) + portfolioRoutes.POST("/:id/approve", h.ApprovePortfolio) + portfolioRoutes.POST("/merge", h.MergePortfolios) + portfolioRoutes.POST("/compare", h.ComparePortfolios) + } +} + +func registerAcademyRoutes(v1 *gin.RouterGroup, h *handlers.AcademyHandlers) { + academyRoutes := v1.Group("/academy") + { + academyRoutes.POST("/courses", h.CreateCourse) + academyRoutes.GET("/courses", h.ListCourses) + academyRoutes.GET("/courses/:id", h.GetCourse) + academyRoutes.PUT("/courses/:id", h.UpdateCourse) + academyRoutes.DELETE("/courses/:id", h.DeleteCourse) + academyRoutes.POST("/enrollments", h.CreateEnrollment) + academyRoutes.GET("/enrollments", h.ListEnrollments) + academyRoutes.PUT("/enrollments/:id/progress", h.UpdateProgress) + academyRoutes.POST("/enrollments/:id/complete", h.CompleteEnrollment) + academyRoutes.GET("/certificates/:id", h.GetCertificate) + academyRoutes.POST("/enrollments/:id/certificate", h.GenerateCertificate) + academyRoutes.POST("/courses/:id/quiz", h.SubmitQuiz) + academyRoutes.PUT("/lessons/:id", h.UpdateLesson) + academyRoutes.POST("/lessons/:id/quiz-test", h.TestQuiz) + academyRoutes.GET("/stats", h.GetStatistics) + academyRoutes.POST("/courses/generate", h.GenerateCourseFromTraining) + academyRoutes.POST("/courses/generate-all", h.GenerateAllCourses) + academyRoutes.GET("/certificates/:id/pdf", h.DownloadCertificatePDF) + } +} + +func registerTrainingRoutes(v1 *gin.RouterGroup, h *handlers.TrainingHandlers) { + trainingRoutes := v1.Group("/training") + { + trainingRoutes.GET("/modules", h.ListModules) + trainingRoutes.GET("/modules/:id", h.GetModule) + trainingRoutes.POST("/modules", h.CreateModule) + trainingRoutes.PUT("/modules/:id", h.UpdateModule) + trainingRoutes.DELETE("/modules/:id", h.DeleteModule) + trainingRoutes.GET("/matrix", h.GetMatrix) + trainingRoutes.GET("/matrix/:role", h.GetMatrixForRole) + trainingRoutes.POST("/matrix", h.SetMatrixEntry) + trainingRoutes.DELETE("/matrix/:role/:moduleId", h.DeleteMatrixEntry) + trainingRoutes.POST("/assignments/compute", h.ComputeAssignments) + trainingRoutes.GET("/assignments", h.ListAssignments) + trainingRoutes.GET("/assignments/:id", h.GetAssignment) + trainingRoutes.POST("/assignments/:id/start", h.StartAssignment) + trainingRoutes.POST("/assignments/:id/progress", h.UpdateAssignmentProgress) + trainingRoutes.POST("/assignments/:id/complete", h.CompleteAssignment) + trainingRoutes.PUT("/assignments/:id", h.UpdateAssignment) + trainingRoutes.GET("/quiz/:moduleId", h.GetQuiz) + trainingRoutes.POST("/quiz/:moduleId/submit", h.SubmitQuiz) + trainingRoutes.GET("/quiz/attempts/:assignmentId", h.GetQuizAttempts) + trainingRoutes.POST("/content/generate", h.GenerateContent) + trainingRoutes.POST("/content/generate-quiz", h.GenerateQuiz) + trainingRoutes.POST("/content/generate-all", h.GenerateAllContent) + trainingRoutes.POST("/content/generate-all-quiz", h.GenerateAllQuizzes) + trainingRoutes.GET("/content/:moduleId", h.GetContent) + trainingRoutes.POST("/content/:moduleId/publish", func(c *gin.Context) { + c.Params = append(c.Params, gin.Param{Key: "id", Value: c.Param("moduleId")}) + h.PublishContent(c) + }) + trainingRoutes.POST("/content/:moduleId/generate-audio", h.GenerateAudio) + trainingRoutes.POST("/content/:moduleId/generate-video", h.GenerateVideo) + trainingRoutes.POST("/content/:moduleId/preview-script", h.PreviewVideoScript) + trainingRoutes.GET("/media/module/:moduleId", h.GetModuleMedia) + trainingRoutes.GET("/media/:mediaId/url", func(c *gin.Context) { + c.Params = append(c.Params, gin.Param{Key: "id", Value: c.Param("mediaId")}) + h.GetMediaURL(c) + }) + trainingRoutes.POST("/media/:mediaId/publish", func(c *gin.Context) { + c.Params = append(c.Params, gin.Param{Key: "id", Value: c.Param("mediaId")}) + h.PublishMedia(c) + }) + trainingRoutes.GET("/media/:mediaId/stream", func(c *gin.Context) { + c.Params = append(c.Params, gin.Param{Key: "id", Value: c.Param("mediaId")}) + h.StreamMedia(c) + }) + trainingRoutes.GET("/deadlines", h.GetDeadlines) + trainingRoutes.GET("/deadlines/overdue", h.GetOverdueDeadlines) + trainingRoutes.POST("/escalation/check", h.CheckEscalation) + trainingRoutes.GET("/audit-log", h.GetAuditLog) + trainingRoutes.GET("/stats", h.GetStats) + trainingRoutes.POST("/certificates/generate/:assignmentId", h.GenerateCertificate) + trainingRoutes.GET("/certificates", h.ListCertificates) + trainingRoutes.GET("/certificates/:id/verify", h.VerifyCertificate) + trainingRoutes.GET("/certificates/:id/pdf", h.DownloadCertificatePDF) + trainingRoutes.GET("/blocks", h.ListBlockConfigs) + trainingRoutes.POST("/blocks", h.CreateBlockConfig) + trainingRoutes.GET("/blocks/:id", h.GetBlockConfig) + trainingRoutes.PUT("/blocks/:id", h.UpdateBlockConfig) + trainingRoutes.DELETE("/blocks/:id", h.DeleteBlockConfig) + trainingRoutes.POST("/blocks/:id/preview", h.PreviewBlock) + trainingRoutes.POST("/blocks/:id/generate", h.GenerateBlock) + trainingRoutes.GET("/blocks/:id/controls", h.GetBlockControls) + trainingRoutes.GET("/canonical/controls", h.ListCanonicalControls) + trainingRoutes.GET("/canonical/meta", h.GetCanonicalMeta) + trainingRoutes.POST("/content/:moduleId/generate-interactive", h.GenerateInteractiveVideo) + trainingRoutes.GET("/content/:moduleId/interactive-manifest", h.GetInteractiveManifest) + trainingRoutes.POST("/checkpoints/:checkpointId/submit", h.SubmitCheckpointQuiz) + trainingRoutes.GET("/checkpoints/progress/:assignmentId", h.GetCheckpointProgress) + } +} + +func registerWhistleblowerRoutes(v1 *gin.RouterGroup, h *handlers.WhistleblowerHandlers) { + wbRoutes := v1.Group("/whistleblower") + { + wbRoutes.POST("/reports/submit", h.SubmitReport) + wbRoutes.GET("/reports/access/:accessKey", h.GetReportByAccessKey) + wbRoutes.POST("/reports/access/:accessKey/messages", h.SendPublicMessage) + wbRoutes.GET("/reports", h.ListReports) + wbRoutes.GET("/reports/:id", h.GetReport) + wbRoutes.PUT("/reports/:id", h.UpdateReport) + wbRoutes.DELETE("/reports/:id", h.DeleteReport) + wbRoutes.POST("/reports/:id/acknowledge", h.AcknowledgeReport) + wbRoutes.POST("/reports/:id/investigate", h.StartInvestigation) + wbRoutes.POST("/reports/:id/measures", h.AddMeasure) + wbRoutes.POST("/reports/:id/close", h.CloseReport) + wbRoutes.POST("/reports/:id/messages", h.SendAdminMessage) + wbRoutes.GET("/reports/:id/messages", h.ListMessages) + wbRoutes.GET("/stats", h.GetStatistics) + } +} + +func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) { + iaceRoutes := v1.Group("/iace") + { + iaceRoutes.GET("/hazard-library", h.ListHazardLibrary) + iaceRoutes.GET("/controls-library", h.ListControlsLibrary) + iaceRoutes.GET("/lifecycle-phases", h.ListLifecyclePhases) + iaceRoutes.GET("/roles", h.ListRoles) + iaceRoutes.GET("/evidence-types", h.ListEvidenceTypes) + iaceRoutes.GET("/protective-measures-library", h.ListProtectiveMeasures) + iaceRoutes.GET("/component-library", h.ListComponentLibrary) + iaceRoutes.GET("/energy-sources", h.ListEnergySources) + iaceRoutes.GET("/tags", h.ListTags) + iaceRoutes.GET("/hazard-patterns", h.ListHazardPatterns) + iaceRoutes.POST("/projects", h.CreateProject) + iaceRoutes.GET("/projects", h.ListProjects) + iaceRoutes.GET("/projects/:id", h.GetProject) + iaceRoutes.PUT("/projects/:id", h.UpdateProject) + iaceRoutes.DELETE("/projects/:id", h.ArchiveProject) + iaceRoutes.POST("/projects/:id/init-from-profile", h.InitFromProfile) + iaceRoutes.POST("/projects/:id/completeness-check", h.CheckCompleteness) + iaceRoutes.POST("/projects/:id/components", h.CreateComponent) + iaceRoutes.GET("/projects/:id/components", h.ListComponents) + iaceRoutes.PUT("/projects/:id/components/:cid", h.UpdateComponent) + iaceRoutes.DELETE("/projects/:id/components/:cid", h.DeleteComponent) + iaceRoutes.POST("/projects/:id/classify", h.Classify) + iaceRoutes.GET("/projects/:id/classifications", h.GetClassifications) + iaceRoutes.POST("/projects/:id/classify/:regulation", h.ClassifySingle) + iaceRoutes.POST("/projects/:id/hazards", h.CreateHazard) + iaceRoutes.GET("/projects/:id/hazards", h.ListHazards) + iaceRoutes.PUT("/projects/:id/hazards/:hid", h.UpdateHazard) + iaceRoutes.POST("/projects/:id/hazards/suggest", h.SuggestHazards) + iaceRoutes.POST("/projects/:id/match-patterns", h.MatchPatterns) + iaceRoutes.POST("/projects/:id/apply-patterns", h.ApplyPatternResults) + iaceRoutes.POST("/projects/:id/hazards/:hid/suggest-measures", h.SuggestMeasuresForHazard) + iaceRoutes.POST("/projects/:id/mitigations/:mid/suggest-evidence", h.SuggestEvidenceForMitigation) + iaceRoutes.POST("/projects/:id/hazards/:hid/assess", h.AssessRisk) + iaceRoutes.GET("/projects/:id/risk-summary", h.GetRiskSummary) + iaceRoutes.POST("/projects/:id/hazards/:hid/reassess", h.ReassessRisk) + iaceRoutes.POST("/projects/:id/hazards/:hid/mitigations", h.CreateMitigation) + iaceRoutes.PUT("/mitigations/:mid", h.UpdateMitigation) + iaceRoutes.POST("/mitigations/:mid/verify", h.VerifyMitigation) + iaceRoutes.POST("/projects/:id/validate-mitigation-hierarchy", h.ValidateMitigationHierarchy) + iaceRoutes.POST("/projects/:id/evidence", h.UploadEvidence) + iaceRoutes.GET("/projects/:id/evidence", h.ListEvidence) + iaceRoutes.POST("/projects/:id/verification-plan", h.CreateVerificationPlan) + iaceRoutes.PUT("/verification-plan/:vid", h.UpdateVerificationPlan) + iaceRoutes.POST("/verification-plan/:vid/complete", h.CompleteVerification) + iaceRoutes.POST("/projects/:id/tech-file/generate", h.GenerateTechFile) + iaceRoutes.GET("/projects/:id/tech-file", h.ListTechFileSections) + iaceRoutes.PUT("/projects/:id/tech-file/:section", h.UpdateTechFileSection) + iaceRoutes.POST("/projects/:id/tech-file/:section/approve", h.ApproveTechFileSection) + iaceRoutes.POST("/projects/:id/tech-file/:section/generate", h.GenerateSingleSection) + iaceRoutes.GET("/projects/:id/tech-file/export", h.ExportTechFile) + iaceRoutes.POST("/projects/:id/monitoring", h.CreateMonitoringEvent) + iaceRoutes.GET("/projects/:id/monitoring", h.ListMonitoringEvents) + iaceRoutes.PUT("/projects/:id/monitoring/:eid", h.UpdateMonitoringEvent) + iaceRoutes.GET("/projects/:id/audit-trail", h.GetAuditTrail) + iaceRoutes.POST("/library-search", h.SearchLibrary) + iaceRoutes.POST("/projects/:id/tech-file/:section/enrich", h.EnrichTechFileSection) + } +}