package router import ( "github.com/atlasos/calypso/internal/common/config" "github.com/atlasos/calypso/internal/common/database" "github.com/atlasos/calypso/internal/common/logger" "github.com/atlasos/calypso/internal/audit" "github.com/atlasos/calypso/internal/auth" "github.com/atlasos/calypso/internal/iam" "github.com/atlasos/calypso/internal/scst" "github.com/atlasos/calypso/internal/storage" "github.com/atlasos/calypso/internal/system" "github.com/atlasos/calypso/internal/tape_physical" "github.com/atlasos/calypso/internal/tape_vtl" "github.com/atlasos/calypso/internal/tasks" "github.com/gin-gonic/gin" ) // NewRouter creates and configures the HTTP router func NewRouter(cfg *config.Config, db *database.DB, log *logger.Logger) *gin.Engine { if cfg.Logging.Level == "debug" { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } r := gin.New() // Middleware r.Use(ginLogger(log)) r.Use(gin.Recovery()) r.Use(corsMiddleware()) // Health check (no auth required) r.GET("/api/v1/health", healthHandler(db)) // API v1 routes v1 := r.Group("/api/v1") { // Auth routes (public) authHandler := auth.NewHandler(db, cfg, log) v1.POST("/auth/login", authHandler.Login) v1.POST("/auth/logout", authHandler.Logout) // Audit middleware for mutating operations (applied to all v1 routes) auditMiddleware := audit.NewMiddleware(db, log) v1.Use(auditMiddleware.LogRequest()) // Protected routes protected := v1.Group("") protected.Use(authMiddleware(authHandler)) protected.Use(func(c *gin.Context) { // Store DB in context for permission middleware c.Set("db", db) c.Next() }) { // Auth protected.GET("/auth/me", authHandler.Me) // Tasks taskHandler := tasks.NewHandler(db, log) protected.GET("/tasks/:id", taskHandler.GetTask) // Storage storageHandler := storage.NewHandler(db, log) storageGroup := protected.Group("/storage") storageGroup.Use(requirePermission("storage", "read")) { storageGroup.GET("/disks", storageHandler.ListDisks) storageGroup.POST("/disks/sync", storageHandler.SyncDisks) storageGroup.GET("/volume-groups", storageHandler.ListVolumeGroups) storageGroup.GET("/repositories", storageHandler.ListRepositories) storageGroup.GET("/repositories/:id", storageHandler.GetRepository) storageGroup.POST("/repositories", storageHandler.CreateRepository) storageGroup.DELETE("/repositories/:id", storageHandler.DeleteRepository) } // SCST scstHandler := scst.NewHandler(db, log) scstGroup := protected.Group("/scst") scstGroup.Use(requirePermission("iscsi", "read")) { scstGroup.GET("/targets", scstHandler.ListTargets) scstGroup.GET("/targets/:id", scstHandler.GetTarget) scstGroup.POST("/targets", scstHandler.CreateTarget) scstGroup.POST("/targets/:id/luns", scstHandler.AddLUN) scstGroup.POST("/targets/:id/initiators", scstHandler.AddInitiator) scstGroup.POST("/config/apply", scstHandler.ApplyConfig) scstGroup.GET("/handlers", scstHandler.ListHandlers) } // Physical Tape Libraries tapeHandler := tape_physical.NewHandler(db, log) tapeGroup := protected.Group("/tape/physical") tapeGroup.Use(requirePermission("tape", "read")) { tapeGroup.GET("/libraries", tapeHandler.ListLibraries) tapeGroup.POST("/libraries/discover", tapeHandler.DiscoverLibraries) tapeGroup.GET("/libraries/:id", tapeHandler.GetLibrary) tapeGroup.POST("/libraries/:id/inventory", tapeHandler.PerformInventory) tapeGroup.POST("/libraries/:id/load", tapeHandler.LoadTape) tapeGroup.POST("/libraries/:id/unload", tapeHandler.UnloadTape) } // Virtual Tape Libraries vtlHandler := tape_vtl.NewHandler(db, log) vtlGroup := protected.Group("/tape/vtl") vtlGroup.Use(requirePermission("tape", "read")) { vtlGroup.GET("/libraries", vtlHandler.ListLibraries) vtlGroup.POST("/libraries", vtlHandler.CreateLibrary) vtlGroup.GET("/libraries/:id", vtlHandler.GetLibrary) vtlGroup.DELETE("/libraries/:id", vtlHandler.DeleteLibrary) vtlGroup.GET("/libraries/:id/drives", vtlHandler.GetLibraryDrives) vtlGroup.GET("/libraries/:id/tapes", vtlHandler.GetLibraryTapes) vtlGroup.POST("/libraries/:id/tapes", vtlHandler.CreateTape) vtlGroup.POST("/libraries/:id/load", vtlHandler.LoadTape) vtlGroup.POST("/libraries/:id/unload", vtlHandler.UnloadTape) } // System Management systemHandler := system.NewHandler(log, tasks.NewEngine(db, log)) systemGroup := protected.Group("/system") systemGroup.Use(requirePermission("system", "read")) { systemGroup.GET("/services", systemHandler.ListServices) systemGroup.GET("/services/:name", systemHandler.GetServiceStatus) systemGroup.POST("/services/:name/restart", systemHandler.RestartService) systemGroup.GET("/services/:name/logs", systemHandler.GetServiceLogs) systemGroup.POST("/support-bundle", systemHandler.GenerateSupportBundle) } // IAM (admin only) iamHandler := iam.NewHandler(db, log) iamGroup := protected.Group("/iam") iamGroup.Use(requireRole("admin")) { iamGroup.GET("/users", iamHandler.ListUsers) iamGroup.GET("/users/:id", iamHandler.GetUser) iamGroup.POST("/users", iamHandler.CreateUser) iamGroup.PUT("/users/:id", iamHandler.UpdateUser) iamGroup.DELETE("/users/:id", iamHandler.DeleteUser) } } } return r } // ginLogger creates a Gin middleware for logging func ginLogger(log *logger.Logger) gin.HandlerFunc { return func(c *gin.Context) { c.Next() log.Info("HTTP request", "method", c.Request.Method, "path", c.Request.URL.Path, "status", c.Writer.Status(), "client_ip", c.ClientIP(), "latency_ms", c.Writer.Size(), ) } } // corsMiddleware adds CORS headers func corsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() } } // healthHandler returns system health status func healthHandler(db *database.DB) gin.HandlerFunc { return func(c *gin.Context) { // Check database connection if err := db.Ping(); err != nil { c.JSON(503, gin.H{ "status": "unhealthy", "error": "database connection failed", }) return } c.JSON(200, gin.H{ "status": "healthy", "service": "calypso-api", }) } }