package api import ( "context" "fmt" "os" "strconv" "strings" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "veza-backend-api/internal/config" "veza-backend-api/internal/database" "veza-backend-api/internal/handlers" "veza-backend-api/internal/middleware" "veza-backend-api/internal/repositories" "veza-backend-api/internal/services" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" authcore "veza-backend-api/internal/core/auth" ) // APIRouter gère la configuration des routes de l'API type APIRouter struct { db *database.Database config *config.Config engine *gin.Engine logger *zap.Logger versionManager *VersionManager // BE-SVC-019: API versioning manager monitoringService *services.MonitoringAlertingService // INT-021: API monitoring and alerting authService *authcore.AuthService // Set by setupAuthRoutes for admin unlock notificationService *services.NotificationService // Shared for N1.2 Web Push pushService *services.PushService // N1 Web Push } // NewAPIRouter crée une nouvelle instance de APIRouter func NewAPIRouter(db *database.Database, cfg *config.Config) *APIRouter { logger := zap.L() return &APIRouter{ db: db, config: cfg, logger: logger, versionManager: NewVersionManager(logger), // BE-SVC-019: Initialize version manager } } // applyCSRFProtection applique le middleware CSRF à un groupe de routes protégées // BE-SEC-004: Ensure all POST/PUT/DELETE endpoints validate CSRF tokens // INT-AUTH-001: Fail-fast in production if Redis unavailable (CSRF requires Redis) func (r *APIRouter) applyCSRFProtection(protectedGroup *gin.RouterGroup) { if r.config == nil { if r.logger != nil { r.logger.Error("CSRF protection cannot be applied: config is nil") } // In production, fail-fast (but we can't check env if config is nil) // This should not happen in normal operation, but we log it return } if r.config.RedisClient == nil { // In non-production, log warning and continue without CSRF // Production case is handled by early validation in Setup() (audit 1.4) if r.logger != nil { r.logger.Warn("Redis not available - CSRF protection disabled (non-production environment)") } return } // CSRF protection active if r.logger != nil { r.logger.Info("CSRF protection enabled", zap.String("environment", r.config.Env), ) } csrfMiddleware := middleware.NewCSRFMiddleware(r.config.RedisClient, r.logger) // MVP: Désactiver CSRF en développement csrfMiddleware.SetEnvironment(r.config.Env) protectedGroup.Use(csrfMiddleware.Middleware()) } // getUploadConfigWithEnv charge la configuration d'upload depuis l'environnement // Cette fonction garantit que ENABLE_CLAMAV et CLAMAV_REQUIRED sont correctement appliqués func getUploadConfigWithEnv() *services.UploadConfig { uploadConfig := services.DefaultUploadConfig() // Lire ENABLE_CLAMAV depuis l'environnement (défaut: true pour sécurité en production) envValue := os.Getenv("ENABLE_CLAMAV") zap.L().Debug("ENABLE_CLAMAV from env", zap.String("value", envValue)) clamAVEnabled := getEnvBool("ENABLE_CLAMAV", true) zap.L().Debug("ENABLE_CLAMAV parsed", zap.Bool("value", clamAVEnabled)) uploadConfig.ClamAVEnabled = clamAVEnabled // Lire CLAMAV_REQUIRED depuis l'environnement (défaut: true pour sécurité) clamAVRequired := getEnvBool("CLAMAV_REQUIRED", true) uploadConfig.ClamAVRequired = clamAVRequired zap.L().Info("Upload config final", zap.Bool("clamav_enabled", uploadConfig.ClamAVEnabled), zap.Bool("clamav_required", uploadConfig.ClamAVRequired)) return uploadConfig } // getEnvBool récupère une variable d'environnement booléenne avec une valeur par défaut func getEnvBool(key string, defaultValue bool) bool { value := os.Getenv(key) if value == "" { zap.L().Debug("Env var undefined, using default", zap.String("key", key), zap.Bool("default", defaultValue)) return defaultValue } // Nettoyer la valeur (trim spaces) value = strings.TrimSpace(value) zap.L().Debug("Env var trimmed", zap.String("key", key), zap.String("value", value)) if boolValue, err := strconv.ParseBool(value); err == nil { zap.L().Debug("Env var parsed", zap.String("key", key), zap.Bool("value", boolValue)) return boolValue } zap.L().Warn("Env var parse error, using default", zap.String("key", key), zap.String("value", value), zap.Bool("default", defaultValue)) return defaultValue } // Setup configure toutes les routes de l'API func (r *APIRouter) Setup(router *gin.Engine) error { r.engine = router // Audit 1.4 P0: Graceful error if Redis down in production (no panic/Fatal) if r.config != nil && r.config.Env == config.EnvProduction && r.config.RedisClient == nil { return fmt.Errorf("CSRF protection requires Redis in production. Redis is unavailable") } // INT-021: Initialize monitoring and alerting service // Initialize monitoring service if Prometheus URL is configured prometheusURL := os.Getenv("PROMETHEUS_URL") if prometheusURL != "" { monitoringConfig := services.MonitoringConfig{ PrometheusURL: prometheusURL, Logger: r.logger, } monitoringService, err := services.NewMonitoringAlertingService(monitoringConfig) if err != nil { r.logger.Warn("Failed to initialize monitoring service", zap.Error(err)) } else { r.monitoringService = monitoringService // Add default alert rules for _, rule := range services.GetDefaultAlertRules() { monitoringService.AddAlertRule(rule) } // Start monitoring in background go func() { ctx := context.Background() if err := monitoringService.StartMonitoring(ctx, 30*time.Second); err != nil { r.logger.Error("Monitoring service stopped", zap.Error(err)) } }() r.logger.Info("Monitoring and alerting service initialized", zap.String("prometheus_url", prometheusURL)) } } else { r.logger.Info("Monitoring service disabled (PROMETHEUS_URL not configured)") } // P1.1: CORS middleware MUST be first to ensure headers are always present // Even if subsequent middlewares reject the request (panic, timeout, error), // the CORS headers will be set, preventing intermittent CORS errors // SECURITY: CORS configuration - use config.CORSOrigins strictly (P0-SECURITY) // No fallback to CORSDefault() to avoid wildcard in production // MOD-P0-001: Apply CORS middleware even if CORSOrigins is empty (strict mode - reject all origins) // The middleware itself handles empty list correctly (rejects all origins) if r.config != nil { // INT-018: Validate CORS configuration before applying middleware // In production, this will fail startup if CORS is misconfigured if err := middleware.ValidateCORSConfiguration(r.config.CORSOrigins, r.config.Env, r.logger); err != nil { // In production, fail startup if CORS is misconfigured if r.config.Env == "production" { r.logger.Fatal("CORS configuration validation failed - startup aborted", zap.Error(err)) } else { // In development/staging, log error but continue r.logger.Error("CORS configuration validation failed", zap.Error(err)) } } router.Use(middleware.CORS(r.config.CORSOrigins)) if len(r.config.CORSOrigins) == 0 { r.logger.Warn("CORS origins not configured - strict mode enabled: ALL CORS requests will be rejected.") } } else { // Fallback: if config is nil, apply CORS with empty list (strict mode) router.Use(middleware.CORS([]string{})) r.logger.Warn("Config is nil - CORS middleware applied in strict mode (reject all origins).") } // Middlewares globaux (after CORS) router.Use(middleware.RequestLogger(r.logger)) // Utilisation du structured logger router.Use(middleware.Metrics()) // Prometheus Metrics router.Use(middleware.SentryRecover(r.logger)) // Sentry error tracking router.Use(middleware.SecurityHeaders()) // MOD-P2-005: Security headers (HSTS, CSP, etc.) // INT-021: Add API monitoring middleware to track failures and trigger alerts router.Use(middleware.APIMonitoringMiddleware(r.logger, r.monitoringService)) // MOD-P1-005: Determine if stack traces should be included in logs // Stack traces only in dev/DEBUG mode (not in production) // Include if: APP_ENV=development OR LOG_LEVEL=DEBUG // MOD-P1-005: Determine if stack traces should be included in logs // Stack traces only in dev/DEBUG mode (not in production) includeStackTrace := r.config.Env == config.EnvDevelopment || r.config.LogLevel == "DEBUG" router.Use(middleware.ErrorHandler(r.logger, r.config.ErrorMetrics, includeStackTrace)) router.Use(middleware.Recovery(r.logger, includeStackTrace)) router.Use(middleware.RequestID()) // Global Timeout middleware (PR-6) // MOD-P0-003: Removed duplicate timeout middleware registration router.Use(middleware.Timeout(r.config.HandlerTimeout)) // Rate limiting via config.RateLimiter si disponible, sinon utiliser SimpleRateLimiter // Toujours actif (A04) — limites assouplies en dev via config if r.config != nil { if r.config.RateLimiter != nil { router.Use(r.config.RateLimiter.RateLimitMiddleware()) } else if r.config.SimpleRateLimiter != nil { router.Use(r.config.SimpleRateLimiter.Middleware()) } } // Swagger Documentation — disabled in production (A05) if r.config == nil || (r.config.Env != config.EnvProduction && r.config.Env != "prod") { swaggerHandler := func(c *gin.Context) { if c.Param("any") == "/doc.json" { if _, err := os.Stat("./docs/swagger.json"); err == nil { c.File("./docs/swagger.json") return } } ginSwagger.WrapHandler(swaggerFiles.Handler)(c) } router.GET("/swagger/*any", swaggerHandler) router.GET("/docs", ginSwagger.WrapHandler(swaggerFiles.Handler)) router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) } // BE-SVC-019: API versioning endpoint (before version middleware) router.GET("/api/versions", VersionInfoHandler(r.versionManager)) // BE-SVC-019: Apply version middleware to API routes router.Use(VersionMiddleware(r.versionManager)) // P1.6: Health endpoint for Docker/K8s healthchecks // Must be before other routes to avoid middleware overhead // DUPLICATE REMOVED to fix panic. // Routes core publiques (health, metrics, upload info) r.setupCorePublicRoutes(router) // Setup internal routes (both legacy and modern) before v1 group // These need to be on the root router, not under /api/v1 r.setupInternalRoutes(router) // Groupe API v1 (nouveau frontend React) v1 := router.Group("/api/v1") { // Auth routes first so r.authService is set for admin unlock in setupCoreProtectedRoutes if err := r.setupAuthRoutes(v1); err != nil { return err } // Routes core protégées (sessions, uploads, audit, admin, conversations) r.setupCoreProtectedRoutes(v1) // Action 5.2.1.1: Validation endpoint for pre-validation r.setupValidateRoutes(v1) // Réactivation des routes User et Track pour Phase 1 r.setupUserRoutes(v1) r.setupTrackRoutes(v1) // BE-API-007: Roles management routes r.setupRoleRoutes(v1) // Réactivation des routes Chat pour Phase 4 r.setupChatRoutes(v1) // Réactivation des routes Playlists pour Phase 5 r.setupPlaylistRoutes(v1) // Réactivation des routes Webhooks r.setupWebhookRoutes(v1) // Marketplace Routes (v1.2.0) r.setupMarketplaceRoutes(v1) // BE-API-035: Analytics routes r.setupAnalyticsRoutes(v1) // Social Routes r.setupSocialRoutes(v1) // Inventory / Gear Routes r.setupGearRoutes(v1) // Queue Routes r.setupQueueRoutes(v1) // Developer Portal (API Keys) r.setupDeveloperRoutes(v1) // Live Streams Routes r.setupLiveRoutes(v1) // Unified search GET /search (tracks, users, playlists) r.setupSearchRoutes(v1) } return nil } // setupChatRoutes configure les routes de chat func (r *APIRouter) setupChatRoutes(router *gin.RouterGroup) { // BE-API-006: Use NewChatServiceWithDB to enable stats functionality chatService := services.NewChatServiceWithDB(r.config.ChatJWTSecret, r.db.GormDB, r.logger) userRepo := repositories.NewGormUserRepository(r.db.GormDB) userService := services.NewUserServiceWithDB(userRepo, r.db.GormDB) chatHandler := handlers.NewChatHandler(chatService, userService, r.logger) chat := router.Group("/chat") { if r.config.AuthMiddleware != nil { chat.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(chat) chat.POST("/token", chatHandler.GetToken) chat.GET("/stats", chatHandler.GetStats) // BE-API-006: Chat stats endpoint } } }