1129 lines
44 KiB
Go
1129 lines
44 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http" // MOD-P2-006: Pour pprof
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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/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"
|
|
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
|
|
}
|
|
|
|
// NewAPIRouter crée une nouvelle instance de APIRouter
|
|
func NewAPIRouter(db *database.Database, cfg *config.Config) *APIRouter {
|
|
return &APIRouter{
|
|
db: db,
|
|
config: cfg,
|
|
logger: zap.L(),
|
|
}
|
|
}
|
|
|
|
// applyCSRFProtection applique le middleware CSRF à un groupe de routes protégées
|
|
// BE-SEC-004: Ensure all POST/PUT/DELETE endpoints validate CSRF tokens
|
|
func (r *APIRouter) applyCSRFProtection(protectedGroup *gin.RouterGroup) {
|
|
if r.config == nil || r.config.RedisClient == nil {
|
|
// Redis non disponible, pas de protection CSRF
|
|
return
|
|
}
|
|
|
|
csrfMiddleware := middleware.NewCSRFMiddleware(r.config.RedisClient, r.logger)
|
|
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
|
|
|
|
// Middlewares globaux
|
|
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.)
|
|
|
|
// 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))
|
|
// 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 {
|
|
// MVP-014: Validate CORS configuration before applying middleware
|
|
// This will log warnings if credentials=true is used with wildcard origins
|
|
if err := middleware.ValidateCORSConfiguration(r.config.CORSOrigins, r.logger); err != nil {
|
|
// In production, we might want to fail startup, but for MVP we just log
|
|
// The validation function logs warnings but doesn't return errors for MVP
|
|
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).")
|
|
}
|
|
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
|
|
if r.config != nil && r.config.RateLimiter != nil {
|
|
router.Use(r.config.RateLimiter.RateLimitMiddleware())
|
|
} else if r.config != nil && r.config.SimpleRateLimiter != nil {
|
|
router.Use(r.config.SimpleRateLimiter.Middleware())
|
|
}
|
|
|
|
// Swagger Documentation
|
|
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
|
|
|
// 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")
|
|
{
|
|
// Routes core protégées (sessions, uploads, audit, admin, conversations)
|
|
r.setupCoreProtectedRoutes(v1)
|
|
|
|
if err := r.setupAuthRoutes(v1); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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 {
|
|
accountLockoutService := services.NewAccountLockoutService(r.config.RedisClient, r.logger)
|
|
authService.SetAccountLockoutService(accountLockoutService)
|
|
} else {
|
|
r.logger.Warn("Redis not available - account lockout disabled")
|
|
}
|
|
|
|
// 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
|
|
registerGroup := authGroup.Group("/register")
|
|
if r.config.EndpointLimiter != nil {
|
|
registerGroup.Use(r.config.EndpointLimiter.RegisterRateLimit())
|
|
}
|
|
registerGroup.POST("", handlers.Register(authService, r.logger))
|
|
|
|
// 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))
|
|
|
|
authGroup.POST("/refresh", handlers.Refresh(authService, r.logger))
|
|
|
|
// 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))
|
|
|
|
// 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))
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
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)
|
|
|
|
// 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)
|
|
|
|
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("/:userId/roles", roleHandler.AssignRole) // POST /api/v1/users/:userId/roles
|
|
protected.DELETE("/:userId/roles/:roleId", roleHandler.RevokeRole) // DELETE /api/v1/users/:userId/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("/:userId/avatar", avatarHandler.UploadAvatar) // BE-API-021: Upload avatar endpoint
|
|
protected.DELETE("/:userId/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)
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
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())
|
|
}
|
|
}
|
|
|
|
// 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-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
|
|
}
|
|
healthHandler := handlers.NewHealthHandler(r.db.GormDB, r.logger, redisClient, rabbitMQEventBus, env)
|
|
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)
|
|
|
|
// Wrap handlers with deprecation middleware for legacy routes only
|
|
router.GET("/health", deprecationMW, healthCheckHandler)
|
|
router.GET("/healthz", deprecationMW, livenessHandler)
|
|
router.GET("/readyz", deprecationMW, 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())
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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)
|
|
var csrfMiddleware *middleware.CSRFMiddleware
|
|
if r.config.RedisClient != nil {
|
|
csrfMiddleware = middleware.NewCSRFMiddleware(r.config.RedisClient, r.logger)
|
|
csrfHandler := handlers.NewCSRFHandler(csrfMiddleware, r.logger)
|
|
|
|
// Route CSRF token (doit être accessible sans vérification CSRF)
|
|
protected.GET("/csrf-token", csrfHandler.GetCSRFToken())
|
|
|
|
// Appliquer le middleware CSRF à toutes les routes protégées (sauf /csrf-token qui est déjà définie)
|
|
protected.Use(csrfMiddleware.Middleware())
|
|
} else {
|
|
r.logger.Warn("Redis not available - CSRF protection disabled")
|
|
}
|
|
// 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())
|
|
sessions.GET("/", sessionHandler.GetSessions())
|
|
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())
|
|
}
|
|
|
|
// 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.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
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
}
|