package api import ( "context" "fmt" "net/http" // MOD-P2-006: Pour pprof "os" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/redis/go-redis/v9" "go.uber.org/zap" "veza-backend-api/internal/config" "veza-backend-api/internal/database" "veza-backend-api/internal/handlers" // Single handlers import "veza-backend-api/internal/middleware" "veza-backend-api/internal/models" "veza-backend-api/internal/repositories" // swaggerFiles "github.com/swaggo/files" // Uncommented // ginSwagger "github.com/swaggo/gin-swagger" // Uncommented // Add missing imports. swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" authcore "veza-backend-api/internal/core/auth" "veza-backend-api/internal/core/marketplace" socialcore "veza-backend-api/internal/core/social" trackcore "veza-backend-api/internal/core/track" "veza-backend-api/internal/services" "veza-backend-api/internal/validators" "veza-backend-api/internal/workers" // swaggerFiles "github.com/swaggo/files" // ginSwagger "github.com/swaggo/gin-swagger" ) // 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 } // 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 { // INT-AUTH-001: In production, Redis MUST be available for CSRF protection if r.config.Env == config.EnvProduction { if r.logger != nil { r.logger.Fatal("CSRF protection requires Redis but Redis is unavailable in production. This is a security requirement.") } panic("CSRF protection requires Redis in production environment. Redis is unavailable.") } // In non-production, log warning and continue without CSRF 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") fmt.Printf("🔍 [ROUTER] ENABLE_CLAMAV depuis env: '%s'\n", envValue) clamAVEnabled := getEnvBool("ENABLE_CLAMAV", true) fmt.Printf("🔍 [ROUTER] ENABLE_CLAMAV parsé: %v\n", clamAVEnabled) uploadConfig.ClamAVEnabled = clamAVEnabled // Lire CLAMAV_REQUIRED depuis l'environnement (défaut: true pour sécurité) clamAVRequired := getEnvBool("CLAMAV_REQUIRED", true) uploadConfig.ClamAVRequired = clamAVRequired fmt.Printf("🔧 [ROUTER] Configuration finale - ClamAVEnabled=%v, ClamAVRequired=%v\n", uploadConfig.ClamAVEnabled, 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 == "" { fmt.Printf("🔍 [ROUTER] Variable %s non définie, utilisation défaut: %v\n", key, defaultValue) return defaultValue } // Nettoyer la valeur (trim spaces) value = strings.TrimSpace(value) fmt.Printf("🔍 [ROUTER] Variable %s='%s' (trimmed)\n", key, value) if boolValue, err := strconv.ParseBool(value); err == nil { fmt.Printf("🔍 [ROUTER] Variable %s parsée: %v\n", key, boolValue) return boolValue } fmt.Printf("⚠️ [ROUTER] Erreur parsing %s='%s', utilisation défaut: %v\n", key, value, defaultValue) return defaultValue } // Setup configure toutes les routes de l'API func (r *APIRouter) Setup(router *gin.Engine) error { r.engine = router // 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 // DÉSACTIVER en développement pour faciliter les tests if r.config != nil && r.config.Env != config.EnvDevelopment { if r.config.RateLimiter != nil { router.Use(r.config.RateLimiter.RateLimitMiddleware()) } else if r.config.SimpleRateLimiter != nil { router.Use(r.config.SimpleRateLimiter.Middleware()) } } else { r.logger.Info("Rate limiting disabled in development mode") } // Swagger Documentation // INT-DOC-001: Custom handler for Swagger routes with fallback for doc.json swaggerHandler := func(c *gin.Context) { // If requesting doc.json specifically, try to serve the static file first if c.Param("any") == "/doc.json" { // Check if file exists before serving if _, err := os.Stat("./docs/swagger.json"); err == nil { // File exists, serve it directly c.File("./docs/swagger.json") return } // File doesn't exist, fall back to gin-swagger } // For all other Swagger routes, use gin-swagger ginSwagger.WrapHandler(swaggerFiles.Handler)(c) } router.GET("/swagger/*any", swaggerHandler) // INT-DOC-001: Expose /docs endpoint as alias for Swagger UI 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) } return nil } // Méthodes de configuration des routes par module // setupMarketplaceRoutes configure les routes de la marketplace func (r *APIRouter) setupMarketplaceRoutes(router *gin.RouterGroup) { uploadDir := r.config.UploadDir if uploadDir == "" { uploadDir = "uploads/tracks" } // Storage service (reused from tracks logic) storageService := services.NewTrackStorageService(uploadDir, false, r.logger) // Marketplace service marketService := marketplace.NewService(r.db.GormDB, r.logger, storageService) marketHandler := handlers.NewMarketplaceHandler(marketService, r.logger) group := router.Group("/marketplace") // Public routes group.GET("/products", marketHandler.ListProducts) // Protected routes if r.config.AuthMiddleware != nil { protected := group.Group("") protected.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(protected) // GO-012: Create product requires creator/premium/admin role createGroup := protected.Group("") createGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole()) createGroup.POST("/products", marketHandler.CreateProduct) // BE-API-037: Update product endpoint (requires ownership) // Resolver: Load product from DB to get its seller_id productOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { productIDStr := c.Param("id") productID, err := uuid.Parse(productIDStr) if err != nil { return uuid.Nil, err } // Load product to get seller ID product, err := marketService.GetProduct(c.Request.Context(), productID) if err != nil { return uuid.Nil, err } return product.SellerID, nil } protected.PUT("/products/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("product", productOwnerResolver), marketHandler.UpdateProduct) // BE-API-038: List orders endpoint protected.GET("/orders", marketHandler.ListOrders) // BE-API-039: Get order details endpoint protected.GET("/orders/:id", marketHandler.GetOrder) protected.POST("/orders", marketHandler.CreateOrder) protected.GET("/download/:product_id", marketHandler.GetDownloadURL) } } // setupAuthRoutes configure les routes d'authentification avec toutes les dépendances func (r *APIRouter) setupAuthRoutes(router *gin.RouterGroup) error { // 1. Instanciation des dépendances emailValidator := validators.NewEmailValidator(r.db.GormDB) passwordValidator := validators.NewPasswordValidator() passwordService := services.NewPasswordService(r.db, r.logger) passwordResetService := services.NewPasswordResetService(r.db, r.logger) jwtService, err := services.NewJWTService(r.config.JWTSecret, r.config.JWTIssuer, r.config.JWTAudience) if err != nil { return fmt.Errorf("failed to initialize JWT service: %w", err) } refreshTokenService := services.NewRefreshTokenService(r.db.GormDB) emailVerificationService := services.NewEmailVerificationService(r.db, r.logger) emailService := services.NewEmailService(r.db, r.logger) sessionService := services.NewSessionService(r.db, r.logger) // 2. Service Auth complet authService := authcore.NewAuthService( r.db.GormDB, emailValidator, passwordValidator, passwordService, jwtService, refreshTokenService, emailVerificationService, passwordResetService, emailService, r.config.JobWorker, // Passer le JobWorker r.logger, ) // BE-SEC-007: Initialize account lockout service and set it on auth service if r.config.RedisClient != nil { lockoutConfig := &services.AccountLockoutConfig{ MaxAttempts: 5, LockoutDuration: 30 * time.Minute, WindowDuration: 15 * time.Minute, ExemptEmails: r.config.AccountLockoutExemptEmails, } accountLockoutService := services.NewAccountLockoutServiceWithConfig(r.config.RedisClient, r.logger, lockoutConfig) authService.SetAccountLockoutService(accountLockoutService) } else { r.logger.Warn("Redis not available - account lockout disabled") } r.authService = authService // 2.5. User Service for GetMe endpoint userRepo := repositories.NewGormUserRepository(r.db.GormDB) userService := services.NewUserServiceWithDB(userRepo, r.db.GormDB) // 3. Handlers authGroup := router.Group("/auth") { // BE-SEC-005: Apply rate limiting to register endpoint // DÉSACTIVER en développement pour faciliter les tests registerGroup := authGroup.Group("/register") if r.config.EndpointLimiter != nil && r.config.Env != config.EnvDevelopment { registerGroup.Use(r.config.EndpointLimiter.RegisterRateLimit()) } registerGroup.POST("", handlers.Register(authService, sessionService, r.logger, r.config)) // BE-API-001: Initialize 2FA service for login handler twoFactorService := services.NewTwoFactorService(r.db, r.logger) // Apply rate limiting to login endpoint (PR-3) loginGroup := authGroup.Group("/login") if r.config.EndpointLimiter != nil { loginGroup.Use(r.config.EndpointLimiter.LoginRateLimit()) } loginGroup.POST("", handlers.Login(authService, sessionService, twoFactorService, r.logger, r.config)) authGroup.POST("/refresh", handlers.Refresh(authService, sessionService, r.logger, r.config)) // BE-SEC-005: Apply rate limiting to email verification endpoints verifyEmailGroup := authGroup.Group("/verify-email") if r.config.EndpointLimiter != nil { verifyEmailGroup.Use(r.config.EndpointLimiter.VerifyEmailRateLimit()) } verifyEmailGroup.POST("", handlers.VerifyEmail(authService)) resendVerificationGroup := authGroup.Group("/resend-verification") if r.config.EndpointLimiter != nil { resendVerificationGroup.Use(r.config.EndpointLimiter.ResendVerificationRateLimit()) } resendVerificationGroup.POST("", handlers.ResendVerification(authService, r.logger)) authGroup.GET("/check-username", handlers.CheckUsername(authService)) // BE-API-042: OAuth routes // Initialize OAuth service jwtSecretBytes := []byte(r.config.JWTSecret) oauthService := services.NewOAuthService(r.db, r.logger, jwtSecretBytes) // Initialize OAuth configs if credentials are available // Note: OAuth credentials should be in environment variables // For MVP, we'll get from environment variables directly baseURL := os.Getenv("BASE_URL") if baseURL == "" { baseURL = "http://localhost:8080" // Default for development } // Get OAuth credentials from environment variables googleClientID := os.Getenv("OAUTH_GOOGLE_CLIENT_ID") googleClientSecret := os.Getenv("OAUTH_GOOGLE_CLIENT_SECRET") githubClientID := os.Getenv("OAUTH_GITHUB_CLIENT_ID") githubClientSecret := os.Getenv("OAUTH_GITHUB_CLIENT_SECRET") discordClientID := os.Getenv("OAUTH_DISCORD_CLIENT_ID") discordClientSecret := os.Getenv("OAUTH_DISCORD_CLIENT_SECRET") if googleClientID != "" && googleClientSecret != "" { oauthService.InitializeConfigs(googleClientID, googleClientSecret, githubClientID, githubClientSecret, discordClientID, discordClientSecret, baseURL) } oauthHandler := handlers.NewOAuthHandler(oauthService, r.logger) oauthGroup := authGroup.Group("/oauth") { // Get available OAuth providers oauthGroup.GET("/providers", oauthHandler.GetOAuthProviders) // Initiate OAuth flow for a provider oauthGroup.GET("/:provider", oauthHandler.InitiateOAuth) // Handle OAuth callback - BE-API-042: Implement OAuth callback endpoint oauthGroup.GET("/:provider/callback", oauthHandler.OAuthCallback) } // Password reset routes (public) // BE-SEC-005: Apply rate limiting to password reset endpoints passwordGroup := authGroup.Group("/password") if r.config.EndpointLimiter != nil { passwordGroup.Use(r.config.EndpointLimiter.PasswordResetRateLimit()) } { // BE-SEC-013: Create auditService for password reset handlers auditService := services.NewAuditService(r.db, r.logger) passwordGroup.POST("/reset-request", handlers.RequestPasswordReset( passwordResetService, passwordService, emailService, auditService, r.logger, )) passwordGroup.POST("/reset", handlers.ResetPassword( passwordResetService, passwordService, authService, sessionService, auditService, r.logger, )) } // Protected routes (authentification JWT requise) protected := authGroup.Group("") protected.Use(r.config.AuthMiddleware.RequireAuth()) // Changed to RequireAuth() // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(protected) { protected.POST("/logout", handlers.Logout(authService, sessionService, r.logger, r.config)) protected.GET("/me", handlers.GetMe(userService)) // BE-API-001: 2FA routes (reuse service created above) twoFactorHandler := handlers.NewTwoFactorHandler(twoFactorService, userService, r.logger) { protected.POST("/2fa/setup", twoFactorHandler.SetupTwoFactor) protected.POST("/2fa/verify", twoFactorHandler.VerifyTwoFactor) protected.POST("/2fa/disable", twoFactorHandler.DisableTwoFactor) protected.GET("/2fa/status", twoFactorHandler.GetTwoFactorStatus) } } } return nil } // setupValidateRoutes configures the validation endpoint // Action 5.2.1.1: Create /api/v1/validate endpoint func (r *APIRouter) setupValidateRoutes(router *gin.RouterGroup) { validateHandler := handlers.NewValidateHandler(r.logger) // Public endpoint - no auth required for validation router.POST("/validate", validateHandler.Validate) } // setupInternalRoutes configure les routes internal (legacy and modern) // These routes must be on the root router, not under /api/v1 func (r *APIRouter) setupInternalRoutes(router *gin.Engine) { // Create track handler for internal routes uploadDir := r.config.UploadDir if uploadDir == "" { uploadDir = "uploads/tracks" } chunksDir := uploadDir + "/chunks" trackService := trackcore.NewTrackService(r.db.GormDB, r.logger, uploadDir) // BE-SVC-001: Set cache service for TrackService if r.config.CacheService != nil { trackService.SetCacheService(r.config.CacheService) } trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) var redisClient *redis.Client if r.config != nil { redisClient = r.config.RedisClient } chunkService := services.NewTrackChunkService(chunksDir, redisClient, r.logger) likeService := services.NewTrackLikeService(r.db.GormDB, r.logger) streamService := services.NewStreamService(r.config.StreamServerURL, r.logger) trackHandler := trackcore.NewTrackHandler( trackService, trackUploadService, chunkService, likeService, streamService, ) // Deprecated /internal routes (legacy, on root router) internalDeprecated := router.Group("/internal") internalDeprecated.Use(middleware.DeprecationWarning(r.logger)) { internalDeprecated.POST("/tracks/:id/stream-ready", trackHandler.HandleStreamCallback) } // New /api/v1/internal routes (modern, on root router) v1Internal := router.Group("/api/v1/internal") { v1Internal.POST("/tracks/:id/stream-ready", trackHandler.HandleStreamCallback) } } // setupUserRoutes configure les routes utilisateur func (r *APIRouter) setupUserRoutes(router *gin.RouterGroup) { userRepo := repositories.NewGormUserRepository(r.db.GormDB) userService := services.NewUserServiceWithDB(userRepo, r.db.GormDB) profileHandler := handlers.NewProfileHandler(userService, r.logger) // MOD-P1-003: Set permission service for admin check in ownership verification if r.config != nil && r.config.PermissionService != nil { profileHandler.SetPermissionService(r.config.PermissionService) } // BE-API-017: Initialize SocialService for follow/unfollow functionality socialService := services.NewSocialService(r.db, r.logger) profileHandler.SetSocialService(socialService) users := router.Group("/users") { users.GET("", profileHandler.ListUsers) // BE-API-040: User list endpoint users.GET("/:id", profileHandler.GetProfile) users.GET("/by-username/:username", profileHandler.GetProfileByUsername) users.GET("/search", profileHandler.SearchUsers) // BE-API-008: User search endpoint // Protected routes if r.config.AuthMiddleware != nil { protected := users.Group("") protected.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(protected) // T0231/T0232: User settings endpoints (uses authenticated user, no :id) settingsHandler := handlers.NewSettingsHandler(userService, r.logger) protected.GET("/settings", settingsHandler.GetSettings) // GET /api/v1/users/settings protected.PUT("/settings", settingsHandler.UpdateSettings) // PUT /api/v1/users/settings // MOD-P0-003: Apply ownership middleware for PUT /users/:id // Resolver: For users, the :id param is directly the user_id userOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { userIDStr := c.Param("id") return uuid.Parse(userIDStr) } protected.PUT("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("user", userOwnerResolver), profileHandler.UpdateProfile) // BE-API-041: Delete user endpoint (soft delete) protected.DELETE("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("user", userOwnerResolver), profileHandler.DeleteUser) protected.GET("/:id/completion", profileHandler.GetProfileCompletion) // BE-API-017: User follow/unfollow endpoints protected.POST("/:id/follow", profileHandler.FollowUser) // Follow user endpoint protected.DELETE("/:id/follow", profileHandler.UnfollowUser) // Unfollow user endpoint // BE-API-018: User block/unblock endpoints protected.POST("/:id/block", profileHandler.BlockUser) // Block user endpoint protected.DELETE("/:id/block", profileHandler.UnblockUser) // Unblock user endpoint // BE-API-007: User role assignment routes roleService := services.NewRoleService(r.db.GormDB) roleHandler := handlers.NewRoleHandler(roleService, r.logger) protected.POST("/:id/roles", roleHandler.AssignRole) // POST /api/v1/users/:id/roles protected.DELETE("/:id/roles/:roleId", roleHandler.RevokeRole) // DELETE /api/v1/users/:id/roles/:roleId // BE-API-021: Avatar upload endpoint // BE-API-022: Avatar delete endpoint avatarUploadDir := r.config.UploadDir if avatarUploadDir == "" { avatarUploadDir = "uploads/avatars" } imageService := services.NewImageService(avatarUploadDir) avatarHandler := handlers.NewAvatarHandler(imageService, userService) protected.POST("/:id/avatar", avatarHandler.UploadAvatar) // BE-API-021: Upload avatar endpoint protected.DELETE("/:id/avatar", avatarHandler.DeleteAvatar) // BE-API-022: Delete avatar endpoint // BE-API-027: User liked tracks endpoint // Initialize TrackLikeService and minimal TrackHandler for user liked tracks uploadDir := r.config.UploadDir if uploadDir == "" { uploadDir = "uploads/tracks" } likeService := services.NewTrackLikeService(r.db.GormDB, r.logger) trackService := trackcore.NewTrackService(r.db.GormDB, r.logger, uploadDir) // BE-SVC-001: Set cache service for TrackService if r.config.CacheService != nil { trackService.SetCacheService(r.config.CacheService) } trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) var redisClient *redis.Client if r.config != nil { redisClient = r.config.RedisClient } chunksDir := uploadDir + "/chunks" chunkService := services.NewTrackChunkService(chunksDir, redisClient, r.logger) streamService := services.NewStreamService(r.config.StreamServerURL, r.logger) trackHandlerForLikes := trackcore.NewTrackHandler( trackService, trackUploadService, chunkService, likeService, streamService, ) protected.GET("/:id/likes", trackHandlerForLikes.GetUserLikedTracks) // BE-API-027: Get user liked tracks endpoint // BE-SVC-022: Data export endpoint for GDPR compliance dataExportService := services.NewDataExportService(r.db.GormDB, r.logger) exportHandler := func(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID not found"}) return } userUUID, ok := userID.(uuid.UUID) if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) return } // Export user data as JSON file jsonData, err := dataExportService.ExportUserDataAsJSON(c.Request.Context(), userUUID) if err != nil { r.logger.Error("Failed to export user data", zap.Error(err), zap.String("user_id", userUUID.String())) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to export user data"}) return } // Set headers for file download filename := "veza-data-export-" + time.Now().Format("2006-01-02T15-04-05") + ".json" c.Header("Content-Type", "application/json") c.Header("Content-Disposition", `attachment; filename="`+filename+`"`) c.Header("Content-Length", strconv.Itoa(len(jsonData))) // Send JSON file c.Data(http.StatusOK, "application/json", jsonData) } protected.GET("/me/export", exportHandler) // BE-SVC-022: Export user data endpoint } } } // setupRoleRoutes configure les routes de gestion des rôles // BE-API-007: Implement roles management endpoints func (r *APIRouter) setupRoleRoutes(router *gin.RouterGroup) { roleService := services.NewRoleService(r.db.GormDB) roleHandler := handlers.NewRoleHandler(roleService, r.logger) // Roles routes roles := router.Group("/roles") { // Protected routes if r.config.AuthMiddleware != nil { protected := roles.Group("") protected.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(protected) { protected.GET("", roleHandler.GetRoles) // GET /api/v1/roles protected.GET("/:id", roleHandler.GetRole) // GET /api/v1/roles/:id } } } } // setupTrackRoutes configure les routes de gestion des tracks func (r *APIRouter) setupTrackRoutes(router *gin.RouterGroup) { uploadDir := r.config.UploadDir if uploadDir == "" { uploadDir = "uploads/tracks" } chunksDir := uploadDir + "/chunks" trackService := trackcore.NewTrackService(r.db.GormDB, r.logger, uploadDir) // BE-SVC-001: Set cache service for TrackService if r.config.CacheService != nil { trackService.SetCacheService(r.config.CacheService) } trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) var redisClient *redis.Client if r.config != nil { redisClient = r.config.RedisClient } chunkService := services.NewTrackChunkService(chunksDir, redisClient, r.logger) likeService := services.NewTrackLikeService(r.db.GormDB, r.logger) streamService := services.NewStreamService(r.config.StreamServerURL, r.logger) trackHandler := trackcore.NewTrackHandler( trackService, trackUploadService, chunkService, likeService, streamService, ) // MOD-P1-003: Set permission service for admin check in ownership verification if r.config != nil && r.config.PermissionService != nil { trackHandler.SetPermissionService(r.config.PermissionService) } // MOD-P1-001: Set upload validator for ClamAV scan before persistence // CORRECTION: Charger la config depuis l'environnement pour respecter ENABLE_CLAMAV uploadConfig := getUploadConfigWithEnv() fmt.Printf("🔧 [ROUTER] Configuration upload chargée - ClamAVEnabled=%v, ClamAVRequired=%v\n", uploadConfig.ClamAVEnabled, uploadConfig.ClamAVRequired) uploadValidator, err := services.NewUploadValidator(uploadConfig, r.logger) if err != nil { r.logger.Warn("Upload validator created with ClamAV unavailable - uploads will be rejected", zap.Error(err)) uploadConfig.ClamAVEnabled = false uploadValidator, _ = services.NewUploadValidator(uploadConfig, r.logger) } trackHandler.SetUploadValidator(uploadValidator) // BE-API-009: Initialize TrackSearchService for track search functionality trackSearchService := services.NewTrackSearchService(r.db.GormDB) trackHandler.SetSearchService(trackSearchService) // BE-API-014: Initialize TrackVersionService for track version restore functionality trackVersionService := services.NewTrackVersionService(r.db.GormDB, r.logger, uploadDir) trackHandler.SetVersionService(trackVersionService) // BE-API-019: Initialize PlaybackAnalyticsService for track play analytics playbackAnalyticsService := services.NewPlaybackAnalyticsService(r.db.GormDB, r.logger) trackHandler.SetPlaybackAnalyticsService(playbackAnalyticsService) tracks := router.Group("/tracks") { // Public routes tracks.GET("", trackHandler.ListTracks) tracks.GET("/search", trackHandler.SearchTracks) // BE-API-009: Track search endpoint tracks.GET("/:id", trackHandler.GetTrack) tracks.GET("/:id/stats", trackHandler.GetTrackStats) tracks.GET("/:id/history", trackHandler.GetTrackHistory) tracks.GET("/:id/download", trackHandler.DownloadTrack) tracks.GET("/shared/:token", trackHandler.GetSharedTrack) // Protected routes if r.config.AuthMiddleware != nil { protected := tracks.Group("") protected.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(protected) // GO-012: Upload track requires creator/premium/admin role uploadGroup := protected.Group("") uploadGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole()) uploadGroup.POST("", trackHandler.UploadTrack) // MOD-P0-003: Apply ownership middleware for PUT/DELETE /tracks/:id // Resolver: Load track from DB to get its user_id trackOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { trackIDStr := c.Param("id") trackID, err := uuid.Parse(trackIDStr) if err != nil { return uuid.Nil, err } // Load track to get owner ID track, err := trackService.GetTrackByID(c.Request.Context(), trackID) if err != nil { return uuid.Nil, err } return track.UserID, nil } protected.PUT("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), trackHandler.UpdateTrack) protected.DELETE("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), trackHandler.DeleteTrack) // Upload protected.GET("/:id/status", trackHandler.GetUploadStatus) protected.POST("/initiate", trackHandler.InitiateChunkedUpload) protected.POST("/chunk", trackHandler.UploadChunk) protected.POST("/complete", trackHandler.CompleteChunkedUpload) protected.GET("/quota/:id", trackHandler.GetUploadQuota) protected.GET("/resume/:uploadId", trackHandler.ResumeUpload) // Batch operations protected.POST("/batch/delete", trackHandler.BatchDeleteTracks) protected.POST("/batch/update", trackHandler.BatchUpdateTracks) // Social protected.POST("/:id/like", trackHandler.LikeTrack) protected.DELETE("/:id/like", trackHandler.UnlikeTrack) protected.GET("/:id/likes", trackHandler.GetTrackLikes) // Sharing protected.POST("/:id/share", trackHandler.CreateShare) protected.DELETE("/share/:id", trackHandler.RevokeShare) // Versions protected.POST("/:id/versions/:versionId/restore", trackHandler.RestoreVersion) // BE-API-014: Restore track version endpoint // Analytics protected.POST("/:id/play", trackHandler.RecordPlay) // BE-API-019: Record track play event endpoint // HLS Streaming // BE-API-020: HLS stream info and status endpoints hlsOutputDir := r.config.UploadDir if hlsOutputDir == "" { hlsOutputDir = "uploads/tracks" } hlsService := services.NewHLSService(r.db.GormDB, hlsOutputDir, r.logger) hlsHandler := handlers.NewHLSHandler(hlsService) tracks.GET("/:id/hls/info", hlsHandler.GetStreamInfo) // BE-API-020: Get HLS stream info tracks.GET("/:id/hls/status", hlsHandler.GetStreamStatus) // BE-API-020: Get HLS stream status } } // BE-API-013: Setup comment routes commentService := services.NewCommentService(r.db.GormDB, r.logger) commentHandler := handlers.NewCommentHandler(commentService, r.logger) // Comments routes - public GET, protected POST/DELETE comments := router.Group("/tracks") { // Public: Get comments for a track comments.GET("/:id/comments", commentHandler.GetComments) // BE-API-013: GET /api/v1/tracks/:id/comments // Protected: Create and delete comments if r.config.AuthMiddleware != nil { protected := comments.Group("") protected.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(protected) { protected.POST("/:id/comments", commentHandler.CreateComment) // BE-API-013: POST /api/v1/tracks/:id/comments } } } // Comments routes - protected DELETE commentsProtected := router.Group("/comments") { if r.config.AuthMiddleware != nil { commentsProtected.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(commentsProtected) { commentsProtected.DELETE("/:id", commentHandler.DeleteComment) // BE-API-013: DELETE /api/v1/comments/:id } } } // Note: Internal routes are now set up in setupInternalRoutes() to avoid // path prefix issues when setupTrackRoutes is called with a RouterGroup // BE-API-027: User liked tracks route moved to setupUserRoutes } // 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 } } } // setupPlaylistRoutes configure les routes pour les playlists func (r *APIRouter) setupPlaylistRoutes(router *gin.RouterGroup) { playlistRepo := repositories.NewPlaylistRepository(r.db.GormDB) playlistTrackRepo := repositories.NewPlaylistTrackRepository(r.db.GormDB) playlistCollaboratorRepo := repositories.NewPlaylistCollaboratorRepository(r.db.GormDB) userRepo := repositories.NewGormUserRepository(r.db.GormDB) playlistService := services.NewPlaylistService( playlistRepo, playlistTrackRepo, playlistCollaboratorRepo, userRepo, r.logger, ) // BE-API-004: Initialize PlaylistShareService for share link functionality playlistShareService := services.NewPlaylistShareService(r.db.GormDB) playlistService.SetPlaylistShareService(playlistShareService) // BE-API-005: Initialize PlaylistFollowService for recommendations functionality playlistFollowService := services.NewPlaylistFollowService(r.db.GormDB, r.logger) playlistService.SetPlaylistFollowService(playlistFollowService) playlistHandler := handlers.NewPlaylistHandler(playlistService, r.db.GormDB, r.logger) // BE-API-005: Inject PlaylistFollowService into handler for recommendations playlistHandler.SetPlaylistFollowService(playlistFollowService) // Protected routes for playlists playlists := router.Group("/playlists") if r.config.AuthMiddleware != nil { playlists.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(playlists) { playlists.GET("", playlistHandler.GetPlaylists) playlists.POST("", playlistHandler.CreatePlaylist) playlists.GET("/search", playlistHandler.SearchPlaylists) // BE-API-003: Playlist search endpoint playlists.GET("/recommendations", playlistHandler.GetRecommendations) // BE-API-005: Playlist recommendations endpoint playlists.GET("/:id", playlistHandler.GetPlaylist) // BE-SEC-003: Apply ownership middleware for PUT/DELETE /playlists/:id // Resolver: Load playlist from DB to get its user_id playlistOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { playlistIDStr := c.Param("id") playlistID, err := uuid.Parse(playlistIDStr) if err != nil { return uuid.Nil, err } // Load playlist to get owner ID playlist, err := playlistRepo.GetByID(c.Request.Context(), playlistID) if err != nil { return uuid.Nil, err } return playlist.UserID, nil } playlists.PUT("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.UpdatePlaylist) playlists.DELETE("/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.DeletePlaylist) // Playlist Tracks playlists.POST("/:id/tracks", playlistHandler.AddTrack) playlists.DELETE("/:id/tracks/:track_id", playlistHandler.RemoveTrack) playlists.PUT("/:id/tracks/reorder", playlistHandler.ReorderTracks) // Playlist Collaborators // BE-API-002: Add collaborator routes with ownership checks // POST and DELETE require ownership (enforced by service layer, but middleware adds extra security) playlists.POST("/:id/collaborators", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.AddCollaborator) playlists.GET("/:id/collaborators", playlistHandler.GetCollaborators) // GET accessible to collaborators (service checks permissions) playlists.PUT("/:id/collaborators/:userId", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.UpdateCollaboratorPermission) playlists.DELETE("/:id/collaborators/:userId", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.RemoveCollaborator) // BE-API-004: Playlist share link endpoint playlists.POST("/:id/share", r.config.AuthMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), playlistHandler.CreateShareLink) } } } // setupWebhookRoutes configure les routes pour les webhooks func (r *APIRouter) setupWebhookRoutes(router *gin.RouterGroup) { webhookService := services.NewWebhookService(r.db.GormDB, r.logger, r.config.JWTSecret) webhookWorker := workers.NewWebhookWorker( r.db.GormDB, webhookService, r.logger, 100, // Queue size 5, // Workers 3, // Max retries ) // Start worker in background go webhookWorker.Start(context.Background()) webhookHandler := handlers.NewWebhookHandler(webhookService, webhookWorker, r.logger) webhooks := router.Group("/webhooks") if r.config.AuthMiddleware != nil { webhooks.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(webhooks) } { webhooks.POST("", webhookHandler.RegisterWebhook()) webhooks.GET("", webhookHandler.ListWebhooks()) webhooks.DELETE("/:id", webhookHandler.DeleteWebhook()) webhooks.GET("/stats", webhookHandler.GetWebhookStats()) webhooks.POST("/:id/test", webhookHandler.TestWebhook()) // BE-SEC-012: Regenerate API key endpoint webhooks.POST("/:id/regenerate-key", webhookHandler.RegenerateAPIKey()) } } // setupAnalyticsRoutes configure les routes pour les analytics // BE-API-035: Implement analytics events endpoint func (r *APIRouter) setupAnalyticsRoutes(router *gin.RouterGroup) { if r.db == nil || r.db.GormDB == nil { return } // Initialize analytics service analyticsService := services.NewAnalyticsService(r.db.GormDB, r.logger) analyticsHandler := handlers.NewAnalyticsHandler(analyticsService, r.logger) // Set JobWorker if available if r.config != nil && r.config.JobWorker != nil { analyticsHandler.SetJobWorker(r.config.JobWorker) } analytics := router.Group("/analytics") if r.config != nil && r.config.AuthMiddleware != nil { analytics.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(analytics) } { // BE-API-037: GET /analytics - Aggregated analytics endpoint analytics.GET("", analyticsHandler.GetAnalytics) // BE-API-035: Analytics events endpoint analytics.POST("/events", analyticsHandler.RecordEvent) // BE-API-036: Track analytics dashboard endpoint analytics.GET("/tracks/:id", analyticsHandler.GetTrackAnalyticsDashboard) } } // setupCorePublicRoutes configure les routes publiques core (health, metrics, upload info) func (r *APIRouter) setupCorePublicRoutes(router *gin.Engine) { // Health check handlers var healthCheckHandler gin.HandlerFunc var livenessHandler gin.HandlerFunc var readinessHandler gin.HandlerFunc if r.db != nil && r.db.GormDB != nil { var redisClient interface{} if r.config != nil { redisClient = r.config.RedisClient } var rabbitMQEventBus interface{} if r.config != nil { rabbitMQEventBus = r.config.RabbitMQEventBus } var env string if r.config != nil { env = r.config.Env } // BE-SVC-016: Pass additional services for detailed health checks var s3Service interface{} var jobWorker interface{} var emailSender interface{} if r.config != nil { s3Service = r.config.S3StorageService jobWorker = r.config.JobWorker emailSender = r.config.EmailSender } healthHandler := handlers.NewHealthHandlerWithServices( r.db.GormDB, r.logger, redisClient, rabbitMQEventBus, env, s3Service, jobWorker, emailSender, ) healthCheckHandler = healthHandler.Check livenessHandler = healthHandler.Liveness readinessHandler = healthHandler.Readiness } else { healthCheckHandler = handlers.SimpleHealthCheck livenessHandler = handlers.SimpleHealthCheck readinessHandler = handlers.SimpleHealthCheck } // Deprecated Public Core Routes - apply deprecation middleware only to specific routes // Use a wrapper function to apply middleware to individual routes deprecationMW := middleware.DeprecationWarning(r.logger) // INT-021: Add health check monitoring middleware healthMonitoringMW := middleware.HealthCheckMonitoring(r.logger, r.monitoringService) // Wrap handlers with deprecation middleware for legacy routes only router.GET("/health", deprecationMW, healthMonitoringMW, healthCheckHandler) router.GET("/healthz", deprecationMW, healthMonitoringMW, livenessHandler) router.GET("/readyz", deprecationMW, healthMonitoringMW, readinessHandler) router.GET("/metrics", deprecationMW, handlers.PrometheusMetrics()) if r.config != nil && r.config.ErrorMetrics != nil { router.GET("/metrics/aggregated", deprecationMW, handlers.AggregatedMetrics(r.config.ErrorMetrics)) } router.GET("/system/metrics", deprecationMW, handlers.SystemMetrics) // New /api/v1 Public Core Routes v1Public := router.Group("/api/v1") { v1Public.GET("/health", healthCheckHandler) v1Public.GET("/healthz", livenessHandler) v1Public.GET("/readyz", readinessHandler) // Status endpoint (comprehensive health check) if r.db != nil && r.db.GormDB != nil { var redisClient interface{} if r.config != nil { redisClient = r.config.RedisClient } chatServerURL := "" streamServerURL := "" if r.config != nil { chatServerURL = r.config.ChatServerURL streamServerURL = r.config.StreamServerURL } // Get build info from environment or defaults getEnv := func(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } version := getEnv("APP_VERSION", "v1.0.0") gitCommit := getEnv("GIT_COMMIT", "unknown") buildTime := getEnv("BUILD_TIME", "") environment := "" if r.config != nil { environment = r.config.Env } statusHandler := handlers.NewStatusHandler( r.db.GormDB, r.logger, redisClient, chatServerURL, streamServerURL, version, gitCommit, buildTime, environment, ) v1Public.GET("/status", statusHandler.GetStatus) } v1Public.GET("/metrics", handlers.PrometheusMetrics()) if r.config != nil && r.config.ErrorMetrics != nil { v1Public.GET("/metrics/aggregated", handlers.AggregatedMetrics(r.config.ErrorMetrics)) } v1Public.GET("/system/metrics", handlers.SystemMetrics) // Upload info endpoints (public, already in /api/v1) // MOD-P1-001-REFINEMENT: Permettre démarrage même si ClamAV down if r.db != nil && r.db.GormDB != nil { // CORRECTION: Charger la config depuis l'environnement pour respecter ENABLE_CLAMAV uploadConfig := getUploadConfigWithEnv() uploadValidator, err := services.NewUploadValidator(uploadConfig, r.logger) if err != nil { r.logger.Warn("Upload validator created with ClamAV unavailable - uploads will be rejected", zap.Error(err)) // Créer un validateur minimal pour permettre les endpoints info uploadConfig.ClamAVEnabled = false uploadValidator, _ = services.NewUploadValidator(uploadConfig, r.logger) } auditService := services.NewAuditService(r.db, r.logger) trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, trackUploadService, r.logger, r.config.MaxConcurrentUploads) v1Public.GET("/upload/limits", uploadHandler.GetUploadLimits()) v1Public.GET("/upload/validate-type", uploadHandler.ValidateFileType()) } // Frontend logging endpoint (public - frontend needs to send logs) if r.config != nil { frontendLogHandler, err := handlers.NewFrontendLogHandler(r.config, r.logger) if err != nil { r.logger.Warn("Failed to create frontend log handler, frontend logs will not be stored", zap.Error(err)) } else { v1Public.POST("/logs/frontend", frontendLogHandler.ReceiveLog) r.logger.Info("Frontend logging endpoint enabled", zap.String("endpoint", "/api/v1/logs/frontend")) } } } } // setupCoreProtectedRoutes configure les routes protégées core (sessions, uploads, audit, admin, conversations) func (r *APIRouter) setupCoreProtectedRoutes(v1 *gin.RouterGroup) { if r.db == nil || r.db.GormDB == nil || r.config == nil { return } // Route CSRF token (doit être accessible sans authentification pour le login) // Doit être enregistrée AVANT le groupe protected pour ne pas hériter ses middlewares csrfMiddleware := middleware.NewCSRFMiddleware(r.config.RedisClient, r.logger) csrfMiddleware.SetEnvironment(r.config.Env) csrfHandler := handlers.NewCSRFHandler(csrfMiddleware, r.logger) if r.config.AuthMiddleware != nil { v1.GET("/csrf-token", r.config.AuthMiddleware.OptionalAuth(), csrfHandler.GetCSRFToken()) } else { v1.GET("/csrf-token", csrfHandler.GetCSRFToken()) } // Middleware d'authentification pour routes protégées protected := v1.Group("/") if r.config.AuthMiddleware != nil { protected.Use(r.config.AuthMiddleware.RequireAuth()) } // Services nécessaires sessionService := services.NewSessionService(r.db, r.logger) // CSRF Middleware (si Redis est disponible) if r.config.RedisClient != nil { // Appliquer le middleware CSRF à toutes les routes protégées protected.Use(csrfMiddleware.Middleware()) // INT-AUTH-001: Log explicit activation r.logger.Info("CSRF protection enabled for core protected routes", zap.String("environment", r.config.Env), ) } else { // INT-AUTH-001: In production, fail-fast if Redis unavailable if r.config.Env == config.EnvProduction { r.logger.Fatal("CSRF protection requires Redis but Redis is unavailable in production. This is a security requirement.") panic("CSRF protection requires Redis in production environment. Redis is unavailable.") } // In non-production, log warning and continue without CSRF r.logger.Warn("Redis not available - CSRF protection disabled (non-production environment)", zap.String("environment", r.config.Env), ) } // CORRECTION: Charger la config depuis l'environnement pour respecter ENABLE_CLAMAV uploadConfig := getUploadConfigWithEnv() // MOD-P1-001-REFINEMENT: Permettre démarrage même si ClamAV down uploadValidator, err := services.NewUploadValidator(uploadConfig, r.logger) if err != nil { r.logger.Warn("Upload validator created with ClamAV unavailable - uploads will be rejected", zap.Error(err)) // Créer un validateur minimal pour permettre les routes d'upload (qui rejetteront) uploadConfig.ClamAVEnabled = false uploadValidator, _ = services.NewUploadValidator(uploadConfig, r.logger) } auditService := services.NewAuditService(r.db, r.logger) trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger) // Handlers sessionHandler := handlers.NewSessionHandler(sessionService, auditService, r.logger) uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, trackUploadService, r.logger, r.config.MaxConcurrentUploads) auditHandler := handlers.NewAuditHandler(auditService, r.logger) // Routes de session sessions := protected.Group("/sessions") { sessions.POST("/logout", sessionHandler.Logout()) sessions.POST("/logout-all", sessionHandler.LogoutAll()) // ISSUE-007: Ajouter route sans trailing slash pour éviter redirection 301 sessions.GET("", sessionHandler.GetSessions()) sessions.GET("/", sessionHandler.GetSessions()) // Garder aussi avec slash pour compatibilité sessions.DELETE("/:session_id", sessionHandler.RevokeSession()) sessions.GET("/stats", sessionHandler.GetSessionStats()) sessions.POST("/refresh", sessionHandler.RefreshSession()) } // Routes d'upload avec rate limiting spécifique uploads := protected.Group("/uploads") { if r.config.RedisClient != nil { uploads.Use(middleware.UploadRateLimit(r.config.RedisClient)) } uploads.POST("/", uploadHandler.UploadFile()) uploads.POST("/batch", uploadHandler.BatchUpload()) uploads.GET("/:id/status", uploadHandler.GetUploadStatus()) uploads.GET("/:id/progress", uploadHandler.UploadProgress()) uploads.DELETE("/:id", uploadHandler.DeleteUpload()) uploads.GET("/stats", uploadHandler.GetUploadStats()) } // Routes d'audit audit := protected.Group("/audit") { audit.GET("/logs", auditHandler.SearchLogs()) audit.GET("/stats", auditHandler.GetStats()) audit.GET("/activity", auditHandler.GetUserActivity()) audit.GET("/suspicious", auditHandler.DetectSuspiciousActivity()) audit.GET("/ip/:ip", auditHandler.GetIPActivity()) audit.GET("/logs/:id", auditHandler.GetAuditLog()) audit.POST("/cleanup", auditHandler.CleanupOldLogs()) } // Action 2.1.1.2: Dashboard aggregation endpoint // Create track service wrapper for dashboard handler (to avoid import cycle) uploadDir := r.config.UploadDir if uploadDir == "" { uploadDir = "uploads/tracks" } trackServiceForDashboard := trackcore.NewTrackService(r.db.GormDB, r.logger, uploadDir) if r.config.CacheService != nil { trackServiceForDashboard.SetCacheService(r.config.CacheService) } // Create wrapper function that adapts trackcore.TrackListParams to handlers.TrackListParamsForDashboard trackListFunc := func(ctx context.Context, params handlers.TrackListParamsForDashboard) ([]*models.Track, int64, error) { return trackServiceForDashboard.ListTracks(ctx, trackcore.TrackListParams{ Page: params.Page, Limit: params.Limit, UserID: params.UserID, Genre: params.Genre, Format: params.Format, SortBy: params.SortBy, SortOrder: params.SortOrder, }) } dashboardHandler := handlers.NewDashboardHandler(auditService, trackListFunc, r.logger) protected.GET("/dashboard", dashboardHandler.GetDashboard()) // Routes de conversations (chat rooms) roomRepo := repositories.NewRoomRepository(r.db.GormDB) messageRepo := repositories.NewChatMessageRepository(r.db.GormDB) // New roomService := services.NewRoomService(roomRepo, messageRepo, r.logger) // Updated constructor roomHandler := handlers.NewRoomHandler(roomService, r.logger) conversations := protected.Group("/conversations") { conversations.GET("", roomHandler.GetUserRooms) conversations.POST("", roomHandler.CreateRoom) conversations.GET("/:id", roomHandler.GetRoom) conversations.PUT("/:id", roomHandler.UpdateRoom) // BE-API-012: Update conversation endpoint conversations.DELETE("/:id", roomHandler.DeleteRoom) // BE-API-010: Delete conversation endpoint conversations.POST("/:id/members", roomHandler.AddMember) conversations.POST("/:id/participants", roomHandler.AddParticipant) // BE-API-011: Add participant endpoint conversations.DELETE("/:id/participants/:userId", roomHandler.RemoveParticipant) // BE-API-011: Remove participant endpoint conversations.GET("/:id/history", roomHandler.GetRoomHistory) } // BE-API-016: Setup notification routes notificationService := services.NewNotificationService(r.db, r.logger) handlers.NewNotificationHandlers(notificationService) notifications := protected.Group("/notifications") { notifications.GET("", handlers.NotificationHandlersInstance.GetNotifications) // BE-API-016: Get notifications endpoint notifications.GET("/unread-count", handlers.NotificationHandlersInstance.GetUnreadCount) // BE-API-016: Get unread count endpoint notifications.POST("/:id/read", handlers.NotificationHandlersInstance.MarkAsRead) // BE-API-016: Mark notification as read endpoint notifications.POST("/read-all", handlers.NotificationHandlersInstance.MarkAllAsRead) // BE-API-016: Mark all notifications as read endpoint notifications.DELETE("/:id", handlers.NotificationHandlersInstance.DeleteNotification) // BE-API-016: Delete notification endpoint notifications.DELETE("", handlers.NotificationHandlersInstance.DeleteAllNotifications) // BE-API-016: Delete all notifications endpoint } // Routes administrateur (avec authentification + permissions admin) admin := v1.Group("/admin") { if r.config.AuthMiddleware != nil { admin.Use(r.config.AuthMiddleware.RequireAuth()) admin.Use(r.config.AuthMiddleware.RequireAdmin()) } // Audit logs (disponibles) admin.GET("/audit/logs", auditHandler.SearchLogs()) admin.GET("/audit/stats", auditHandler.GetStats()) admin.GET("/audit/suspicious", auditHandler.DetectSuspiciousActivity()) // MOD-P2-006: Profiling pprof (protégé par auth admin) admin.Any("/debug/pprof/*path", gin.WrapH(http.DefaultServeMux)) // BE-SEC-007: Unlock account locked by failed login attempts (admin only) if r.authService != nil { admin.POST("/auth/unlock-account", handlers.UnlockAccount(r.authService, r.logger)) } } } // setupSocialRoutes configure les routes sociales func (r *APIRouter) setupSocialRoutes(router *gin.RouterGroup) { socialService := socialcore.NewService(r.db.GormDB, r.logger) socialHandler := handlers.NewSocialHandler(socialService, r.logger) social := router.Group("/social") { // Public routes social.GET("/feed", socialHandler.GetFeed) social.GET("/posts/user/:user_id", socialHandler.GetPostsByUser) // Protected routes if r.config.AuthMiddleware != nil { protected := social.Group("") protected.Use(r.config.AuthMiddleware.RequireAuth()) // BE-SEC-004: Apply CSRF protection to all state-changing endpoints r.applyCSRFProtection(protected) protected.POST("/posts", socialHandler.CreatePost) protected.POST("/like", socialHandler.ToggleLike) protected.POST("/comments", socialHandler.AddComment) } } }