package router import ( "context" "time" "github.com/atlasos/calypso/internal/audit" "github.com/atlasos/calypso/internal/auth" "github.com/atlasos/calypso/internal/common/cache" "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/iam" "github.com/atlasos/calypso/internal/monitoring" "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() // Initialize cache if enabled var responseCache *cache.Cache if cfg.Server.Cache.Enabled { responseCache = cache.NewCache(cfg.Server.Cache.DefaultTTL) log.Info("Response caching enabled", "default_ttl", cfg.Server.Cache.DefaultTTL) } // Middleware r.Use(ginLogger(log)) r.Use(gin.Recovery()) r.Use(securityHeadersMiddleware(cfg)) r.Use(rateLimitMiddleware(cfg, log)) r.Use(corsMiddleware(cfg)) // Cache control headers (always applied) r.Use(cacheControlMiddleware()) // Response caching middleware (if enabled) if cfg.Server.Cache.Enabled { cacheConfig := CacheConfig{ Enabled: cfg.Server.Cache.Enabled, DefaultTTL: cfg.Server.Cache.DefaultTTL, MaxAge: cfg.Server.Cache.MaxAge, } r.Use(cacheMiddleware(cacheConfig, responseCache)) } // Initialize monitoring services eventHub := monitoring.NewEventHub(log) alertService := monitoring.NewAlertService(db, log) alertService.SetEventHub(eventHub) // Connect alert service to event hub metricsService := monitoring.NewMetricsService(db, log) healthService := monitoring.NewHealthService(db, log, metricsService) // Start event hub in background go eventHub.Run() // Start metrics broadcaster in background go func() { ticker := time.NewTicker(30 * time.Second) // Broadcast metrics every 30 seconds defer ticker.Stop() for { select { case <-ticker.C: if metrics, err := metricsService.CollectMetrics(context.Background()); err == nil { eventHub.BroadcastMetrics(metrics) } } } }() // Initialize and start alert rule engine alertRuleEngine := monitoring.NewAlertRuleEngine(db, log, alertService) // Register default alert rules alertRuleEngine.RegisterRule(monitoring.NewAlertRule( "storage-capacity-warning", "Storage Capacity Warning", monitoring.AlertSourceStorage, &monitoring.StorageCapacityCondition{ThresholdPercent: 80.0}, monitoring.AlertSeverityWarning, true, "Alert when storage repositories exceed 80% capacity", )) alertRuleEngine.RegisterRule(monitoring.NewAlertRule( "storage-capacity-critical", "Storage Capacity Critical", monitoring.AlertSourceStorage, &monitoring.StorageCapacityCondition{ThresholdPercent: 95.0}, monitoring.AlertSeverityCritical, true, "Alert when storage repositories exceed 95% capacity", )) alertRuleEngine.RegisterRule(monitoring.NewAlertRule( "task-failure", "Task Failure", monitoring.AlertSourceTask, &monitoring.TaskFailureCondition{LookbackMinutes: 60}, monitoring.AlertSeverityWarning, true, "Alert when tasks fail within the last hour", )) // Start alert rule engine in background ctx := context.Background() go alertRuleEngine.Start(ctx) // Health check (no auth required) - enhanced r.GET("/api/v1/health", func(c *gin.Context) { health := healthService.CheckHealth(c.Request.Context()) statusCode := 200 if health.Status == "unhealthy" { statusCode = 503 } else if health.Status == "degraded" { statusCode = 200 // Still 200 but with degraded status } c.JSON(statusCode, health) }) // 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) // Pass cache to storage handler for cache invalidation if responseCache != nil { storageHandler.SetCache(responseCache) } // Start disk monitor service in background (syncs disks every 5 minutes) diskMonitor := storage.NewDiskMonitor(db, log, 5*time.Minute) go diskMonitor.Start(context.Background()) // Start ZFS pool monitor service in background (syncs pools every 2 minutes) zfsPoolMonitor := storage.NewZFSPoolMonitor(db, log, 2*time.Minute) go zfsPoolMonitor.Start(context.Background()) 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", requirePermission("storage", "write"), storageHandler.CreateRepository) storageGroup.DELETE("/repositories/:id", requirePermission("storage", "write"), storageHandler.DeleteRepository) // ZFS Pools storageGroup.GET("/zfs/pools", storageHandler.ListZFSPools) storageGroup.GET("/zfs/pools/:id", storageHandler.GetZFSPool) storageGroup.POST("/zfs/pools", requirePermission("storage", "write"), storageHandler.CreateZPool) storageGroup.DELETE("/zfs/pools/:id", requirePermission("storage", "write"), storageHandler.DeleteZFSPool) storageGroup.POST("/zfs/pools/:id/spare", requirePermission("storage", "write"), storageHandler.AddSpareDisk) // ZFS Datasets storageGroup.GET("/zfs/pools/:id/datasets", storageHandler.ListZFSDatasets) storageGroup.POST("/zfs/pools/:id/datasets", requirePermission("storage", "write"), storageHandler.CreateZFSDataset) storageGroup.DELETE("/zfs/pools/:id/datasets/:dataset", requirePermission("storage", "write"), storageHandler.DeleteZFSDataset) // ZFS ARC Stats storageGroup.GET("/zfs/arc/stats", storageHandler.GetARCStats) } // 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) // Start MHVTL monitor service in background (syncs every 5 minutes) mhvtlMonitor := tape_vtl.NewMHVTLMonitor(db, log, "/etc/mhvtl", 5*time.Minute) go mhvtlMonitor.Start(context.Background()) 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) systemGroup.GET("/interfaces", systemHandler.ListNetworkInterfaces) } // IAM (admin only) iamHandler := iam.NewHandler(db, cfg, 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) } // Monitoring monitoringHandler := monitoring.NewHandler(db, log, alertService, metricsService, eventHub) monitoringGroup := protected.Group("/monitoring") monitoringGroup.Use(requirePermission("monitoring", "read")) { // Alerts monitoringGroup.GET("/alerts", monitoringHandler.ListAlerts) monitoringGroup.GET("/alerts/:id", monitoringHandler.GetAlert) monitoringGroup.POST("/alerts/:id/acknowledge", monitoringHandler.AcknowledgeAlert) monitoringGroup.POST("/alerts/:id/resolve", monitoringHandler.ResolveAlert) // Metrics monitoringGroup.GET("/metrics", monitoringHandler.GetMetrics) // WebSocket (no permission check needed, handled by auth middleware) monitoringGroup.GET("/events", monitoringHandler.WebSocketHandler) } } } 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(), ) } }