veza/veza-backend-api/internal/api/router.go
senke b47fa21331 feat(v0.12.8): documentation & API publique — rate limiting, scopes, OpenAPI
- API key rate limiting middleware (1000 reads/h, 200 writes/h par clé)
  — tracking séparé read/write, par API key ID (pas par IP)
  — headers X-RateLimit-Limit/Remaining/Reset sur chaque réponse
- API key scope enforcement middleware (read → GET, write → POST/PUT/DELETE)
  — admin scope permet tout, CSRF skip pour API key auth
- OpenAPI spec: ajout securityDefinition ApiKeyAuth (X-API-Key header)
- Swagger annotations: ajout ApiKeyAuth dans cmd/api/main.go
- Wiring dans router.go: middlewares appliqués sur tout le groupe /api/v1
- Tests: 10 tests (5 rate limiter + 5 scope enforcement), tous PASS

Backend existant déjà en place (pré-v0.12.8):
- Swagger UI (gin-swagger + frontend SwaggerUIDoc component)
- API key CRUD (create/list/delete + X-API-Key auth dans AuthMiddleware)
- Developer Dashboard frontend (API keys, webhooks, playground)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 18:44:09 +01:00

487 lines
18 KiB
Go

package api
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"veza-backend-api/internal/config"
"veza-backend-api/internal/database"
"veza-backend-api/internal/handlers"
"veza-backend-api/internal/middleware"
"veza-backend-api/internal/repositories"
"veza-backend-api/internal/services"
chatws "veza-backend-api/internal/websocket/chat"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
authcore "veza-backend-api/internal/core/auth"
)
// APIRouter gère la configuration des routes de l'API
type APIRouter struct {
db *database.Database
config *config.Config
engine *gin.Engine
logger *zap.Logger
versionManager *VersionManager // BE-SVC-019: API versioning manager
monitoringService *services.MonitoringAlertingService // INT-021: API monitoring and alerting
authService *authcore.AuthService // Set by setupAuthRoutes for admin unlock
notificationService *services.NotificationService // Shared for N1.2 Web Push
pushService *services.PushService // N1 Web Push
}
// NewAPIRouter crée une nouvelle instance de APIRouter
func NewAPIRouter(db *database.Database, cfg *config.Config) *APIRouter {
logger := zap.L()
return &APIRouter{
db: db,
config: cfg,
logger: logger,
versionManager: NewVersionManager(logger), // BE-SVC-019: Initialize version manager
}
}
// applyCSRFProtection applique le middleware CSRF à un groupe de routes protégées
// BE-SEC-004: Ensure all POST/PUT/DELETE endpoints validate CSRF tokens
// INT-AUTH-001: Fail-fast in production if Redis unavailable (CSRF requires Redis)
func (r *APIRouter) applyCSRFProtection(protectedGroup *gin.RouterGroup) {
if r.config == nil {
if r.logger != nil {
r.logger.Error("CSRF protection cannot be applied: config is nil")
}
// In production, fail-fast (but we can't check env if config is nil)
// This should not happen in normal operation, but we log it
return
}
if r.config.RedisClient == nil {
// In non-production, log warning and continue without CSRF
// Production case is handled by early validation in Setup() (audit 1.4)
if r.logger != nil {
r.logger.Warn("Redis not available - CSRF protection disabled (non-production environment)")
}
return
}
// CSRF protection active
if r.logger != nil {
r.logger.Info("CSRF protection enabled",
zap.String("environment", r.config.Env),
)
}
csrfMiddleware := middleware.NewCSRFMiddleware(r.config.RedisClient, r.logger)
// MVP: Désactiver CSRF en développement
csrfMiddleware.SetEnvironment(r.config.Env)
protectedGroup.Use(csrfMiddleware.Middleware())
}
// getUploadConfigWithEnv charge la configuration d'upload depuis l'environnement
// Cette fonction garantit que ENABLE_CLAMAV et CLAMAV_REQUIRED sont correctement appliqués
func getUploadConfigWithEnv() *services.UploadConfig {
uploadConfig := services.DefaultUploadConfig()
// Lire ENABLE_CLAMAV depuis l'environnement (défaut: true pour sécurité en production)
envValue := os.Getenv("ENABLE_CLAMAV")
zap.L().Debug("ENABLE_CLAMAV from env", zap.String("value", envValue))
clamAVEnabled := getEnvBool("ENABLE_CLAMAV", true)
zap.L().Debug("ENABLE_CLAMAV parsed", zap.Bool("value", clamAVEnabled))
uploadConfig.ClamAVEnabled = clamAVEnabled
// Lire CLAMAV_REQUIRED depuis l'environnement (défaut: true pour sécurité)
clamAVRequired := getEnvBool("CLAMAV_REQUIRED", true)
uploadConfig.ClamAVRequired = clamAVRequired
zap.L().Info("Upload config final",
zap.Bool("clamav_enabled", uploadConfig.ClamAVEnabled),
zap.Bool("clamav_required", uploadConfig.ClamAVRequired))
return uploadConfig
}
// getEnvBool récupère une variable d'environnement booléenne avec une valeur par défaut
func getEnvBool(key string, defaultValue bool) bool {
value := os.Getenv(key)
if value == "" {
zap.L().Debug("Env var undefined, using default", zap.String("key", key), zap.Bool("default", defaultValue))
return defaultValue
}
// Nettoyer la valeur (trim spaces)
value = strings.TrimSpace(value)
zap.L().Debug("Env var trimmed", zap.String("key", key), zap.String("value", value))
if boolValue, err := strconv.ParseBool(value); err == nil {
zap.L().Debug("Env var parsed", zap.String("key", key), zap.Bool("value", boolValue))
return boolValue
}
zap.L().Warn("Env var parse error, using default", zap.String("key", key), zap.String("value", value), zap.Bool("default", defaultValue))
return defaultValue
}
// Setup configure toutes les routes de l'API
func (r *APIRouter) Setup(router *gin.Engine) error {
r.engine = router
// Audit 1.4 P0: Graceful error if Redis down in production (no panic/Fatal)
if r.config != nil && r.config.Env == config.EnvProduction && r.config.RedisClient == nil {
return fmt.Errorf("CSRF protection requires Redis in production. Redis is unavailable")
}
// INT-021: Initialize monitoring and alerting service
// Initialize monitoring service if Prometheus URL is configured
prometheusURL := os.Getenv("PROMETHEUS_URL")
if prometheusURL != "" {
monitoringConfig := services.MonitoringConfig{
PrometheusURL: prometheusURL,
Logger: r.logger,
}
monitoringService, err := services.NewMonitoringAlertingService(monitoringConfig)
if err != nil {
r.logger.Warn("Failed to initialize monitoring service", zap.Error(err))
} else {
r.monitoringService = monitoringService
// Add default alert rules
for _, rule := range services.GetDefaultAlertRules() {
monitoringService.AddAlertRule(rule)
}
// Start monitoring in background
go func() {
ctx := context.Background()
if err := monitoringService.StartMonitoring(ctx, 30*time.Second); err != nil {
r.logger.Error("Monitoring service stopped", zap.Error(err))
}
}()
r.logger.Info("Monitoring and alerting service initialized", zap.String("prometheus_url", prometheusURL))
}
} else {
r.logger.Info("Monitoring service disabled (PROMETHEUS_URL not configured)")
}
// P1.1: CORS middleware MUST be first to ensure headers are always present
// Even if subsequent middlewares reject the request (panic, timeout, error),
// the CORS headers will be set, preventing intermittent CORS errors
// SECURITY: CORS configuration - use config.CORSOrigins strictly (P0-SECURITY)
// No fallback to CORSDefault() to avoid wildcard in production
// MOD-P0-001: Apply CORS middleware even if CORSOrigins is empty (strict mode - reject all origins)
// The middleware itself handles empty list correctly (rejects all origins)
if r.config != nil {
// INT-018: Validate CORS configuration before applying middleware
// In production, this will fail startup if CORS is misconfigured
if err := middleware.ValidateCORSConfiguration(r.config.CORSOrigins, r.config.Env, r.logger); err != nil {
// In production, fail startup if CORS is misconfigured
if r.config.Env == "production" {
r.logger.Fatal("CORS configuration validation failed - startup aborted", zap.Error(err))
} else {
// In development/staging, log error but continue
r.logger.Error("CORS configuration validation failed", zap.Error(err))
}
}
router.Use(middleware.CORS(r.config.CORSOrigins))
if len(r.config.CORSOrigins) == 0 {
r.logger.Warn("CORS origins not configured - strict mode enabled: ALL CORS requests will be rejected.")
}
} else {
// Fallback: if config is nil, apply CORS with empty list (strict mode)
router.Use(middleware.CORS([]string{}))
r.logger.Warn("Config is nil - CORS middleware applied in strict mode (reject all origins).")
}
// Middlewares globaux (after CORS)
router.Use(middleware.CacheHeaders(middleware.DefaultCacheHeadersConfig())) // v0.12.4: CDN cache headers
router.Use(middleware.MaintenanceGin()) // v0.803 ADM1-03: Maintenance mode (503 except /health, /admin)
router.Use(middleware.RequestLogger(r.logger)) // Utilisation du structured logger
router.Use(middleware.Metrics()) // Prometheus Metrics
router.Use(middleware.SentryRecover(r.logger)) // Sentry error tracking
router.Use(middleware.SecurityHeaders()) // MOD-P2-005: Security headers (HSTS, CSP, etc.)
router.Use(middleware.CCPA()) // v0.803 SEC2-06: CCPA Do Not Sell (Sec-GPC)
// v0.803 SEC2-03: HTTP audit middleware for auto-logging POST/PUT/DELETE
if r.config != nil && r.config.AuditService != nil {
router.Use(middleware.AuditMiddleware(r.config.AuditService, r.logger))
}
// INT-021: Add API monitoring middleware to track failures and trigger alerts
router.Use(middleware.APIMonitoringMiddleware(r.logger, r.monitoringService))
// MOD-P1-005: Determine if stack traces should be included in logs
// Stack traces only in dev/DEBUG mode (not in production)
// Include if: APP_ENV=development OR LOG_LEVEL=DEBUG
// MOD-P1-005: Determine if stack traces should be included in logs
// Stack traces only in dev/DEBUG mode (not in production)
includeStackTrace := r.config.Env == config.EnvDevelopment || r.config.LogLevel == "DEBUG"
router.Use(middleware.ErrorHandler(r.logger, r.config.ErrorMetrics, includeStackTrace))
router.Use(middleware.Recovery(r.logger, includeStackTrace))
router.Use(middleware.RequestID())
// Global Timeout middleware (PR-6)
// MOD-P0-003: Removed duplicate timeout middleware registration
router.Use(middleware.Timeout(r.config.HandlerTimeout))
// v0.803 SEC1-04: DDoS rate limiting (1000 req/s global, 100 req/s per-IP)
if r.config != nil && r.config.RedisClient != nil {
router.Use(middleware.DDoSRateLimitMiddleware(r.config.RedisClient))
}
// Rate limiting via config.RateLimiter si disponible, sinon utiliser SimpleRateLimiter
// Toujours actif (A04) — limites assouplies en dev via config
if r.config != nil {
if r.config.RateLimiter != nil {
router.Use(r.config.RateLimiter.RateLimitMiddleware())
} else if r.config.SimpleRateLimiter != nil {
router.Use(r.config.SimpleRateLimiter.Middleware())
}
}
// v0.12.4: Response cache for public GET endpoints (Redis-backed)
if r.config != nil && r.config.RedisClient != nil {
router.Use(middleware.ResponseCache(middleware.ResponseCacheConfig{
RedisClient: r.config.RedisClient,
Logger: r.logger,
DefaultTTL: 5 * time.Minute,
KeyPrefix: "http_cache",
EndpointTTLs: map[string]time.Duration{
"/api/v1/tracks": 15 * time.Minute,
"/api/v1/discover": 10 * time.Minute,
"/api/v1/search": 5 * time.Minute,
"/api/v1/users": 5 * time.Minute,
},
}))
}
// Swagger Documentation — disabled in production (A05)
if r.config == nil || (r.config.Env != config.EnvProduction && r.config.Env != "prod") {
swaggerHandler := func(c *gin.Context) {
if c.Param("any") == "/doc.json" {
if _, err := os.Stat("./docs/swagger.json"); err == nil {
c.File("./docs/swagger.json")
return
}
}
ginSwagger.WrapHandler(swaggerFiles.Handler)(c)
}
router.GET("/swagger/*any", swaggerHandler)
router.GET("/docs", ginSwagger.WrapHandler(swaggerFiles.Handler))
router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
// BE-SVC-019: API versioning endpoint (before version middleware)
router.GET("/api/versions", VersionInfoHandler(r.versionManager))
// BE-SVC-019: Apply version middleware to API routes
router.Use(VersionMiddleware(r.versionManager))
// P1.6: Health endpoint for Docker/K8s healthchecks
// Must be before other routes to avoid middleware overhead
// DUPLICATE REMOVED to fix panic.
// Routes core publiques (health, metrics, upload info)
r.setupCorePublicRoutes(router)
// Setup internal routes (both legacy and modern) before v1 group
// These need to be on the root router, not under /api/v1
r.setupInternalRoutes(router)
// Groupe API v1 (nouveau frontend React)
v1 := router.Group("/api/v1")
// v0.12.8: API key rate limiting — applied to all v1 routes
// Only affects requests authenticated via X-API-Key (JWT requests pass through)
apiKeyRateLimiter := middleware.NewAPIKeyRateLimiter(middleware.DefaultAPIKeyRateLimiterConfig())
v1.Use(apiKeyRateLimiter.Middleware())
// v0.12.8: API key scope enforcement — check read/write scopes on API key requests
if r.config != nil && r.config.AuthMiddleware != nil {
apiKeySvc := services.NewAPIKeyService(r.db.GormDB, r.logger)
v1.Use(middleware.RequireAPIKeyScope(apiKeySvc))
}
{
// Auth routes first so r.authService is set for admin unlock in setupCoreProtectedRoutes
if err := r.setupAuthRoutes(v1); err != nil {
return err
}
// Routes core protégées (sessions, uploads, audit, admin, conversations)
r.setupCoreProtectedRoutes(v1)
// Action 5.2.1.1: Validation endpoint for pre-validation
r.setupValidateRoutes(v1)
// Réactivation des routes User et Track pour Phase 1
r.setupUserRoutes(v1)
r.setupTrackRoutes(v1)
// BE-API-007: Roles management routes
r.setupRoleRoutes(v1)
// Réactivation des routes Chat pour Phase 4
r.setupChatRoutes(v1)
// v0.502: Chat WebSocket endpoint (replaces Rust chat server)
r.setupChatWebSocket(v1)
// Réactivation des routes Playlists pour Phase 5
r.setupPlaylistRoutes(v1)
// Réactivation des routes Webhooks
r.setupWebhookRoutes(v1)
// Marketplace Routes (v1.2.0)
r.setupMarketplaceRoutes(v1)
// BE-API-035: Analytics routes
r.setupAnalyticsRoutes(v1)
// Social Routes
r.setupSocialRoutes(v1)
// Feed Routes (v0.10.0 F210: tracks from followed users)
r.setupFeedRoutes(v1)
// Discover Routes (v0.10.1 F351-F355: tags, genres, browse)
r.setupDiscoverRoutes(v1)
// Inventory / Gear Routes
r.setupGearRoutes(v1)
// Queue Routes
r.setupQueueRoutes(v1)
// Developer Portal (API Keys)
r.setupDeveloperRoutes(v1)
// Live Streams Routes
r.setupLiveRoutes(v1)
// Co-listening (v0.10.7 F481)
r.setupCoListeningRoutes(v1)
// Cloud Storage Routes (v0.501 C1)
r.setupCloudRoutes(v1)
// Tag suggestions (v0.802 FM1-03)
r.setupTagRoutes(v1)
// Unified search GET /search (tracks, users, playlists)
r.setupSearchRoutes(v1)
// v0.11.2: Advanced Moderation (F411-F420)
r.setupModerationRoutes(v1)
// v0.11.3: Admin Platform Management (F421-F435)
r.setupAdminPlatformRoutes(v1)
// v0.12.1: Subscription Plans & Management (F001-F030)
r.setupSubscriptionRoutes(v1)
// v0.12.2: Distribution to External Platforms (F501-F510)
r.setupDistributionRoutes(v1)
// v0.12.3: Formation & Éducation (F276-F305)
r.setupEducationRoutes(v1)
}
return nil
}
// setupChatWebSocket configures the chat WebSocket endpoint (v0.502)
func (r *APIRouter) setupChatWebSocket(router *gin.RouterGroup) {
chatService := services.NewChatServiceWithDB(r.config.ChatJWTSecret, r.db.GormDB, r.logger)
presenceService := chatws.NewChatPresenceService(r.config.RedisClient, r.logger)
hub := chatws.NewHub(r.logger, presenceService)
go hub.Run()
msgRepo := repositories.NewChatMessageRepository(r.db.GormDB)
readRepo := repositories.NewReadReceiptRepository(r.db.GormDB)
deliveredRepo := repositories.NewDeliveredStatusRepository(r.db.GormDB)
reactionRepo := repositories.NewReactionRepository(r.db.GormDB)
userRepo := repositories.NewGormUserRepository(r.db.GormDB)
pubsub := services.NewChatPubSubService(r.config.RedisClient, r.logger)
permissions := chatws.NewPermissionService(r.db.GormDB, r.logger)
rateLimiter := chatws.NewRateLimiter(r.config.RedisClient, r.logger)
msgHandler := chatws.NewMessageHandler(
hub, msgRepo, readRepo, deliveredRepo, reactionRepo, userRepo,
pubsub, permissions, rateLimiter, r.logger,
)
wsHandler := handlers.NewChatWebSocketHandler(chatService, hub, msgHandler, r.logger)
router.GET("/ws", wsHandler.HandleWebSocket)
// v0.10.5 F551: Inject chat hub into notification service for real-time delivery
if r.notificationService != nil {
r.notificationService.SetWSNotifier(&chatHubNotifierAdapter{hub: hub})
}
r.logger.Info("Chat WebSocket endpoint registered at /api/v1/ws")
}
// chatHubNotifierAdapter adapts chatws.Hub to services.NotificationWSNotifier (F551)
type chatHubNotifierAdapter struct {
hub *chatws.Hub
}
func (a *chatHubNotifierAdapter) NotifyUser(userID uuid.UUID, payload []byte) {
a.hub.SendToUser(userID, payload)
}
// setupChatRoutes configure les routes de chat
func (r *APIRouter) setupChatRoutes(router *gin.RouterGroup) {
// BE-API-006: Use NewChatServiceWithDB to enable stats functionality
chatService := services.NewChatServiceWithDB(r.config.ChatJWTSecret, r.db.GormDB, r.logger)
userRepo := repositories.NewGormUserRepository(r.db.GormDB)
userService := services.NewUserServiceWithDB(userRepo, r.db.GormDB)
chatHandler := handlers.NewChatHandler(chatService, userService, r.logger)
// v0.9.6: Chat reactions REST + message search
reactionRepo := repositories.NewReactionRepository(r.db.GormDB)
msgRepo := repositories.NewChatMessageRepository(r.db.GormDB)
permissions := chatws.NewPermissionService(r.db.GormDB, r.logger)
chatReactionHandler := handlers.NewChatReactionHandler(reactionRepo, msgRepo, permissions, r.logger)
chatSearchHandler := handlers.NewChatSearchHandler(msgRepo, permissions, r.logger)
chat := router.Group("/chat")
{
if r.config.AuthMiddleware != nil {
chat.Use(r.config.AuthMiddleware.RequireAuth())
// BE-SEC-004: Apply CSRF protection to all state-changing endpoints
r.applyCSRFProtection(chat)
chat.POST("/token", chatHandler.GetToken)
chat.GET("/stats", chatHandler.GetStats) // BE-API-006: Chat stats endpoint
// v0.9.6: Chat rooms REST (reactions, search)
// v0.9.7: Chat room attachments (file upload)
rooms := chat.Group("/rooms")
{
rooms.POST("/:roomId/messages/:messageId/reactions", chatReactionHandler.AddReaction)
rooms.DELETE("/:roomId/messages/:messageId/reactions", chatReactionHandler.RemoveReaction)
rooms.GET("/:roomId/messages/search", chatSearchHandler.SearchMessages)
// v0.9.7: Chat attachment upload (mp3, wav, ogg, jpg, png, pdf, max 50MB)
if r.config.S3StorageService != nil {
uploadConfig := getUploadConfigWithEnv()
chatUploadValidator, err := services.NewUploadValidator(uploadConfig, r.logger)
if err != nil {
r.logger.Warn("Chat upload validator unavailable - attachments disabled", zap.Error(err))
} else {
chatAttachmentHandler := handlers.NewChatAttachmentHandler(
r.config.S3StorageService,
permissions,
chatUploadValidator,
r.logger,
)
rooms.POST("/:roomId/attachments", chatAttachmentHandler.UploadChatAttachment)
}
}
}
}
}
}