veza/veza-backend-api/internal/api/router.go
senke 30f17dfc2a chore(backend): config, router, auth, stream service, sanitizer, tests
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 22:19:09 +01:00

1467 lines
59 KiB
Go

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 == "" {
appDomain := os.Getenv("APP_DOMAIN")
if appDomain == "" {
appDomain = "veza.fr"
}
baseURL = "http://" + appDomain + ":8080"
}
// 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, r.config.CORSOrigins)
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.NewStreamServiceWithAPIKey(r.config.StreamServerURL, r.config.StreamServerInternalAPIKey, 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.NewStreamServiceWithAPIKey(r.config.StreamServerURL, r.config.StreamServerInternalAPIKey, 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.NewStreamServiceWithAPIKey(r.config.StreamServerURL, r.config.StreamServerInternalAPIKey, 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)
}
}
}