package api import ( "context" "fmt" "os" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "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" chatws "veza-backend-api/internal/websocket/chat" 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.CacheHeaders(middleware.DefaultCacheHeadersConfig())) // v0.12.4: CDN cache headers router.Use(middleware.MaintenanceGin()) // v0.803 ADM1-03: Maintenance mode (503 except /health, /admin) 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.) router.Use(middleware.CCPA()) // v0.803 SEC2-06: CCPA Do Not Sell (Sec-GPC) // v0.803 SEC2-03: HTTP audit middleware for auto-logging POST/PUT/DELETE if r.config != nil && r.config.AuditService != nil { router.Use(middleware.AuditMiddleware(r.config.AuditService, r.logger)) } // 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)) // v0.803 SEC1-04: DDoS rate limiting (1000 req/s global, 100 req/s per-IP) if r.config != nil && r.config.RedisClient != nil { router.Use(middleware.DDoSRateLimitMiddleware(r.config.RedisClient)) } // 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()) } } // v0.12.4: Response cache for public GET endpoints (Redis-backed) if r.config != nil && r.config.RedisClient != nil { router.Use(middleware.ResponseCache(middleware.ResponseCacheConfig{ RedisClient: r.config.RedisClient, Logger: r.logger, DefaultTTL: 5 * time.Minute, KeyPrefix: "http_cache", EndpointTTLs: map[string]time.Duration{ "/api/v1/tracks": 15 * time.Minute, "/api/v1/discover": 10 * time.Minute, "/api/v1/search": 5 * time.Minute, "/api/v1/users": 5 * time.Minute, }, })) } // 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") // v0.12.8: API key rate limiting — applied to all v1 routes // Only affects requests authenticated via X-API-Key (JWT requests pass through) apiKeyRateLimiter := middleware.NewAPIKeyRateLimiter(middleware.DefaultAPIKeyRateLimiterConfig()) v1.Use(apiKeyRateLimiter.Middleware()) // v0.12.8: API key scope enforcement — check read/write scopes on API key requests if r.config != nil && r.config.AuthMiddleware != nil { apiKeySvc := services.NewAPIKeyService(r.db.GormDB, r.logger) v1.Use(middleware.RequireAPIKeyScope(apiKeySvc)) } { // 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) // v0.502: Chat WebSocket endpoint (replaces Rust chat server) r.setupChatWebSocket(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) // Feed Routes (v0.10.0 F210: tracks from followed users) r.setupFeedRoutes(v1) // Discover Routes (v0.10.1 F351-F355: tags, genres, browse) r.setupDiscoverRoutes(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) // Co-listening (v0.10.7 F481) r.setupCoListeningRoutes(v1) // Cloud Storage Routes (v0.501 C1) r.setupCloudRoutes(v1) // Tag suggestions (v0.802 FM1-03) r.setupTagRoutes(v1) // Unified search GET /search (tracks, users, playlists) r.setupSearchRoutes(v1) // v0.11.2: Advanced Moderation (F411-F420) r.setupModerationRoutes(v1) // v0.11.3: Admin Platform Management (F421-F435) r.setupAdminPlatformRoutes(v1) // v0.12.1: Subscription Plans & Management (F001-F030) r.setupSubscriptionRoutes(v1) // v0.12.2: Distribution to External Platforms (F501-F510) r.setupDistributionRoutes(v1) // v0.12.3: Formation & Éducation (F276-F305) r.setupEducationRoutes(v1) } return nil } // setupChatWebSocket configures the chat WebSocket endpoint (v0.502) func (r *APIRouter) setupChatWebSocket(router *gin.RouterGroup) { chatService := services.NewChatServiceWithDB(r.config.ChatJWTSecret, r.db.GormDB, r.logger) presenceService := chatws.NewChatPresenceService(r.config.RedisClient, r.logger) hub := chatws.NewHub(r.logger, presenceService) go hub.Run() msgRepo := repositories.NewChatMessageRepository(r.db.GormDB) readRepo := repositories.NewReadReceiptRepository(r.db.GormDB) deliveredRepo := repositories.NewDeliveredStatusRepository(r.db.GormDB) reactionRepo := repositories.NewReactionRepository(r.db.GormDB) userRepo := repositories.NewGormUserRepository(r.db.GormDB) pubsub := services.NewChatPubSubService(r.config.RedisClient, r.logger) permissions := chatws.NewPermissionService(r.db.GormDB, r.logger) rateLimiter := chatws.NewRateLimiter(r.config.RedisClient, r.logger) msgHandler := chatws.NewMessageHandler( hub, msgRepo, readRepo, deliveredRepo, reactionRepo, userRepo, pubsub, permissions, rateLimiter, r.logger, ) wsHandler := handlers.NewChatWebSocketHandler(chatService, hub, msgHandler, r.logger) router.GET("/ws", wsHandler.HandleWebSocket) // v0.10.5 F551: Inject chat hub into notification service for real-time delivery if r.notificationService != nil { r.notificationService.SetWSNotifier(&chatHubNotifierAdapter{hub: hub}) } r.logger.Info("Chat WebSocket endpoint registered at /api/v1/ws") } // chatHubNotifierAdapter adapts chatws.Hub to services.NotificationWSNotifier (F551) type chatHubNotifierAdapter struct { hub *chatws.Hub } func (a *chatHubNotifierAdapter) NotifyUser(userID uuid.UUID, payload []byte) { a.hub.SendToUser(userID, payload) } // 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) // v0.9.6: Chat reactions REST + message search reactionRepo := repositories.NewReactionRepository(r.db.GormDB) msgRepo := repositories.NewChatMessageRepository(r.db.GormDB) permissions := chatws.NewPermissionService(r.db.GormDB, r.logger) chatReactionHandler := handlers.NewChatReactionHandler(reactionRepo, msgRepo, permissions, r.logger) chatSearchHandler := handlers.NewChatSearchHandler(msgRepo, permissions, 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 // v0.9.6: Chat rooms REST (reactions, search) // v0.9.7: Chat room attachments (file upload) rooms := chat.Group("/rooms") { rooms.POST("/:roomId/messages/:messageId/reactions", chatReactionHandler.AddReaction) rooms.DELETE("/:roomId/messages/:messageId/reactions", chatReactionHandler.RemoveReaction) rooms.GET("/:roomId/messages/search", chatSearchHandler.SearchMessages) // v0.9.7: Chat attachment upload (mp3, wav, ogg, jpg, png, pdf, max 50MB) if r.config.S3StorageService != nil { uploadConfig := getUploadConfigWithEnv() chatUploadValidator, err := services.NewUploadValidator(uploadConfig, r.logger) if err != nil { r.logger.Warn("Chat upload validator unavailable - attachments disabled", zap.Error(err)) } else { chatAttachmentHandler := handlers.NewChatAttachmentHandler( r.config.S3StorageService, permissions, chatUploadValidator, r.logger, ) rooms.POST("/:roomId/attachments", chatAttachmentHandler.UploadChatAttachment) } } } } } }