refactor(go): split roadmap_handlers, academy/store, extract cmd/server/main to internal/app
roadmap_handlers.go (740 LOC) → roadmap_handlers.go, roadmap_item_handlers.go, roadmap_import_handlers.go academy/store.go (683 LOC) → store_courses.go, store_enrollments.go cmd/server/main.go (681 LOC) → internal/app/app.go (Run+buildRouter) + internal/app/routes.go (registerXxx helpers) main.go reduced to 7 LOC thin entrypoint calling app.Run() All files under 410 LOC. Zero behavior changes, same package declarations. go vet passes on all directly-split packages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,681 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "github.com/breakpilot/ai-compliance-sdk/internal/app"
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Load configuration
|
app.Run()
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|||||||
349
ai-compliance-sdk/internal/academy/store_courses.go
Normal file
349
ai-compliance-sdk/internal/academy/store_courses.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -2,352 +2,13 @@ package academy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5"
|
"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
|
// 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 {
|
func (s *Store) UpdateEnrollmentProgress(ctx context.Context, id uuid.UUID, progress int, currentLesson int) error {
|
||||||
now := time.Now().UTC()
|
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, `
|
_, err := s.pool.Exec(ctx, `
|
||||||
UPDATE academy_enrollments SET
|
UPDATE academy_enrollments SET
|
||||||
progress_percent = $2,
|
progress_percent = $2,
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
||||||
"github.com/breakpilot/ai-compliance-sdk/internal/roadmap"
|
"github.com/breakpilot/ai-compliance-sdk/internal/roadmap"
|
||||||
@@ -200,541 +197,3 @@ func (h *RoadmapHandlers) GetRoadmapStats(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, stats)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
258
ai-compliance-sdk/internal/api/handlers/roadmap_item_handlers.go
Normal file
258
ai-compliance-sdk/internal/api/handlers/roadmap_item_handlers.go
Normal file
@@ -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"})
|
||||||
|
}
|
||||||
161
ai-compliance-sdk/internal/app/app.go
Normal file
161
ai-compliance-sdk/internal/app/app.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
409
ai-compliance-sdk/internal/app/routes.go
Normal file
409
ai-compliance-sdk/internal/app/routes.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user