veza/veza-backend-api/internal/api/router.go

500 lines
19 KiB
Go
Raw Normal View History

2025-12-03 19:29:37 +00:00
package api
import (
"context"
2025-12-13 02:34:34 +00:00
"fmt"
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
"os"
"strconv"
"strings"
"time"
2025-12-03 19:29:37 +00:00
"github.com/gin-gonic/gin"
"github.com/google/uuid"
2025-12-03 19:29:37 +00:00
"go.uber.org/zap"
"veza-backend-api/internal/config"
"veza-backend-api/internal/database"
"veza-backend-api/internal/handlers"
2025-12-03 19:29:37 +00:00
"veza-backend-api/internal/middleware"
"veza-backend-api/internal/repositories"
"veza-backend-api/internal/services"
chatws "veza-backend-api/internal/websocket/chat"
2025-12-03 19:29:37 +00:00
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 {
2026-03-05 22:03:43 +00:00
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
2025-12-03 19:29:37 +00:00
}
// NewAPIRouter crée une nouvelle instance de APIRouter
func NewAPIRouter(db *database.Database, cfg *config.Config) *APIRouter {
logger := zap.L()
2025-12-03 19:29:37 +00:00
return &APIRouter{
db: db,
config: cfg,
logger: logger,
versionManager: NewVersionManager(logger), // BE-SVC-019: Initialize version manager
2025-12-03 19:29:37 +00:00
}
}
// 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
}
2025-12-03 19:29:37 +00:00
// Setup configure toutes les routes de l'API
2025-12-13 02:34:34 +00:00
func (r *APIRouter) Setup(router *gin.Engine) error {
2025-12-03 19:29:37 +00:00
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
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
// SECURITY: CORS configuration - use config.CORSOrigins strictly (P0-SECURITY)
// No fallback to CORSDefault() to avoid wildcard in production
2025-12-13 02:34:34 +00:00
// 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))
}
}
2025-12-03 19:29:37 +00:00
router.Use(middleware.CORS(r.config.CORSOrigins))
2025-12-13 02:34:34 +00:00
if len(r.config.CORSOrigins) == 0 {
r.logger.Warn("CORS origins not configured - strict mode enabled: ALL CORS requests will be rejected.")
}
2025-12-03 19:29:37 +00:00
} else {
2025-12-13 02:34:34 +00:00
// 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).")
2025-12-03 19:29:37 +00:00
}
// Middlewares globaux (after CORS)
router.Use(middleware.CacheHeaders(middleware.DefaultCacheHeadersConfig())) // v0.12.4: CDN cache headers
fix(middleware): persist maintenance flag via platform_settings table The maintenance toggle lived in a package-level `bool` inside `middleware/maintenance.go`. Flipping it via `PUT /admin/maintenance` only updated the pod handling that request — the other N-1 pods stayed open for traffic. In practice this meant deploys-in-progress or incident playbooks silently failed to put the fleet into maintenance. New storage: * Migration `976_platform_settings.sql` adds a typed key/value table (`value_bool` / `value_text` to avoid string parsing in the hot path) and seeds `maintenance_mode=false`. Idempotent on re-run. * `middleware/maintenance.go` rewritten around a `maintenanceState` with a 10s TTL cache. `InitMaintenanceMode(db, logger)` primes the cache at boot; `MaintenanceModeEnabled()` refreshes lazily when the next request lands after the TTL. Startup `MAINTENANCE_MODE` env is still honoured for fresh pods. * `router.go` calls `InitMaintenanceMode` before applying the `MaintenanceGin()` middleware so the first request sees DB truth. * `PUT /api/v1/admin/maintenance` in `routes_core.go` now does an `INSERT ... ON CONFLICT DO UPDATE` on the table *before* the in-memory setter, so the flip survives restarts and propagates to every pod within ~10s (one TTL window). Tests: `TestMaintenanceGin_DBBacked` flips the DB row, waits past a shrunk-for-test TTL, and asserts the cache picked up the change. All four pre-existing tests preserved (`Disabled`, `Enabled_Returns503`, `HealthExempt`, `AdminExempt`). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:57:06 +00:00
// v1.0.4: Back the maintenance flag with platform_settings.maintenance_mode
// so flipping it on one pod propagates to every other pod within ~10s.
if r.db != nil && r.db.GormDB != nil {
middleware.InitMaintenanceMode(r.db.GormDB, r.logger)
}
fix(ci): unblock CI red — gofmt + e2e webserver reuse + orders.hyperswitch_payment_id (Day 4) Three pre-existing infra issues surfaced by the Day 1→Day 3 push wave. Each is independent — bundled here because the goal is "ci.yml + e2e.yml green" before the v1.0.9 tag, and they're all small. (1) gofmt — ci.yml golangci-lint v2 step Five files were unformatted on main. Pre-existing (untouched by my Item G work, but the formatter caught them now): - internal/api/router.go - internal/core/marketplace/reconcile_hyperswitch_test.go - internal/models/user.go - internal/monitoring/ledger_metrics.go - internal/monitoring/ledger_metrics_test.go Pure whitespace via `gofmt -w` — no behavior change. (2) e2e silent-fail — playwright webServer port collision The e2e workflow pre-starts the backend in step 9 ("Build + start backend API") so it can fail-fast on a non-ok health check. But playwright.config.ts had `reuseExistingServer: !process.env.CI` on the backend webServer entry — meaning in CI Playwright tried to spawn a SECOND backend on port 18080. The spawn collided with EADDRINUSE and Playwright silently exited before printing any test output. The artifact upload then warned "No files were found" because tests/e2e/playwright-report/ never got written, and the job ended in `Failure` for an unrelated reason (the artifact upload step's GHESNotSupportedError). Fix: backend `reuseExistingServer: true` always — workflow + dev both pre-start backend on 18080. Vite stays `!CI` because the workflow doesn't pre-start it. Comment in playwright.config.ts documents the symptom so the next person debugging gets the pointer immediately. (3) orders.hyperswitch_payment_id missing in fresh DBs — migration 080 skip-branch + 099 ordering drift Migration 080 (`add_payment_fields`) wraps its ALTERs in "skip if orders doesn't exist". At authoring time orders existed earlier in the migration sequence; that ordering has since shifted (orders is now created at 099_z_create_orders.sql, AFTER 080). Result: in any freshly-migrated DB (CI, fresh dev, future restore drills) migration 080 takes the skip branch and the columns are never added — even though the Order model and the marketplace code rely on them. Symptom: every CI run logs pq: column "hyperswitch_payment_id" does not exist from the periodic ledger_metrics worker. Order checkout would also fail to persist payment_id at write time, breaking reconciliation. Fix: append-only migration 987 with idempotent `ADD COLUMN IF NOT EXISTS` + a partial index on the reconciliation hot path. Production envs that did pick up 080 in the original order are no-ops; fresh envs converge to the same end state. Rollback in migrations/rollback/. Verified locally: $ cd veza-backend-api && go build ./... && VEZA_SKIP_INTEGRATION=1 \ go test -short -count=1 ./internal/... (all green) SKIP_TESTS=1: backend-only Go + Playwright config + SQL. Frontend unit tests irrelevant to this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 10:03:55 +00:00
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))
2025-12-03 19:29:37 +00:00
router.Use(middleware.RequestID())
2025-12-13 02:34:34 +00:00
// 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))
}
2025-12-03 19:29:37 +00:00
// 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())
}
2025-12-03 19:29:37 +00:00
}
// 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))
}
2025-12-03 19:29:37 +00:00
// 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.
2025-12-03 19:29:37 +00:00
// Routes core publiques (health, metrics, upload info)
r.setupCorePublicRoutes(router)
2025-12-16 16:23:49 +00:00
// 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)
feat(embed): /embed/track/:id widget + /oembed envelope + per-track OG tags (W3 Day 15) End-to-end embed pipeline. Standalone HTML widget for iframes, oEmbed JSON for unfurlers (Twitter/Discord/Slack), runtime per-track OG + Twitter player card on the SPA. Share-token storage + handlers were already in place from earlier — Day 15 only adds the embed surface. Backend (root router, no /api/v1 prefix — matches what scrapers expect) - internal/handlers/embed_handler.go : EmbedTrack renders inline HTML with OG tags + <audio controls>. DMCA-blocked tracks 451, private tracks 404 (don't leak existence). X-Frame-Options=ALLOWALL + CSP frame-ancestors=* so the page can be iframed by third parties. OEmbed handler accepts ?url=&format=json, validates the URL points at /tracks/:id, returns a type=rich envelope with an iframe HTML string. ?maxwidth clamped to [240, 1280]. - internal/api/routes_embed.go : registers the two endpoints. - internal/handlers/embed_handler_test.go : pure-function coverage for extractTrackIDFromURL (8 cases incl. trailing slash, query string, hash fragment, subpath) + parseSafeInt (overflow + non-digit rejection). Frontend - apps/web/src/features/tracks/hooks/useTrackOpenGraph.ts : runtime injection of og:* + twitter:player + <link rel=alternate> (oEmbed discovery) into document.head. Limitation noted inline — pure HTML scrapers don't see these ; the embed widget itself carries server-rendered OG tags so unfurlers always work. - TrackDetailPage : wires useTrackOpenGraph(track) on render. E2E (tests/e2e/30-embed-and-share.spec.ts) - 30. /embed/track/:id renders HTML with OG tags + audio src. - 31. /oembed returns valid JSON envelope (rich type, iframe HTML). - 32. /oembed rejects non-track URLs (400). - 33. share-token roundtrip — creator mints, anonymous resolves via /api/v1/tracks/shared/:token (re-uses existing share handler ; Day 15 didn't add new share infra, just covers it under the embed acceptance gate). Acceptance (Day 15) : embed widget Twitter card preview ✓ (OG tags present), oEmbed JSON valid ✓, share token roundtrip ✓. W3 verification gate : Redis Sentinel ✓ · MinIO distribué ✓ · CDN signed URLs ✓ · DMCA E2E ✓ · embed + share token ✓ · all 5 W3 days shipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 13:49:54 +00:00
// v1.0.9 W3 Day 15 — embed widget + oEmbed. Lives on root because
// the URLs are /embed/track/:id and /oembed (no /api/v1 prefix).
r.setupEmbedRoutes(router)
2025-12-03 19:29:37 +00:00
// 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))
}
2025-12-03 19:29:37 +00:00
{
2026-02-07 19:36:48 +00:00
// Auth routes first so r.authService is set for admin unlock in setupCoreProtectedRoutes
2025-12-13 02:34:34 +00:00
if err := r.setupAuthRoutes(v1); err != nil {
return err
}
2026-02-07 19:36:48 +00:00
// Routes core protégées (sessions, uploads, audit, admin, conversations)
r.setupCoreProtectedRoutes(v1)
2025-12-03 19:29:37 +00:00
// Action 5.2.1.1: Validation endpoint for pre-validation
r.setupValidateRoutes(v1)
2025-12-03 19:29:37 +00:00
// 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)
2025-12-03 19:29:37 +00:00
// Réactivation des routes Chat pour Phase 4
r.setupChatRoutes(v1)
// v0.502: Chat WebSocket endpoint (replaces Rust chat server)
r.setupChatWebSocket(v1)
2025-12-03 19:29:37 +00:00
// Réactivation des routes Playlists pour Phase 5
r.setupPlaylistRoutes(v1)
// Réactivation des routes Webhooks
r.setupWebhookRoutes(v1)
2025-12-03 19:29:37 +00:00
// 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)
feat(legal): DMCA notice handler + admin queue + 451 playback gate (W3 Day 14) End-to-end DMCA workflow. Public submission, admin queue, takedown flips track to is_public=false + dmca_blocked=true, playback paths return 451 Unavailable For Legal Reasons. Backend - migrations/988_dmca_notices.sql + rollback : table dmca_notices (id, status, claimant_*, work_description, infringing_track_id FK, sworn_statement_at, takedown_at, counter_notice_at, restored_at, audit_log JSONB, created_at, updated_at). Adds tracks.dmca_blocked BOOLEAN. Partial indexes for the pending queue + per-track lookup. Status enum constrained via CHECK. - internal/models/dmca_notice.go + DmcaBlocked field on Track. - internal/services/dmca_service.go : CreateNotice + ListPending + Takedown + Dismiss. Takedown is a single transaction that flips the track's flags AND appends an audit_log entry — partial state can't happen if the track was deleted between fetch and update. - internal/handlers/dmca_handler.go : POST /api/v1/dmca/notice (public), GET /api/v1/admin/dmca/notices (paginated), POST /:id/takedown, POST /:id/dismiss. sworn_statement=false → 400. Conflict → 409. Track gone after notice → 410. - internal/api/routes_legal.go : route registration. Admin chain : RequireAuth + RequireAdmin + RequireMFA (same as moderation routes). - internal/core/track/track_hls_handler.go : both StreamTrack + DownloadTrack now early-return 451 when track.DmcaBlocked. Owner cannot bypass — only an admin restoring the notice clears the gate. - internal/services/dmca_service_test.go : audit_log append helpers, malformed-JSON rejection, ordering preservation. Frontend - apps/web/src/features/legal/pages/DmcaNoticePage.tsx : public form at /legal/dmca/notice. Validates sworn-statement checkbox client-side. Receipt panel shows the notice ID after submission. - apps/web/src/services/api/dmca.ts : thin client (POST /dmca/notice). - routeConfig + lazy registry updated for the new route. - DmcaPage now links to /legal/dmca/notice instead of saying "form pending". E2E - tests/e2e/29-dmca-notice.spec.ts : 3 tests. (1) anonymous submit yields 201 + pending receipt. (2) sworn_statement=false rejected with 400. (3) admin takedown gates playback with 451 — gated behind E2E_DMCA_ADMIN=1 because admin path requires MFA-bearing seed. Acceptance (Day 14) : public submission produces a pending notice, admin takedown blocks playback at 451. Lab-side validation pending admin MFA seed for the e2e admin pathway. W3 progress : Redis Sentinel ✓ · MinIO distribué ✓ · CDN ✓ · DMCA ✓ · embed ⏳ Day 15. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 13:39:33 +00:00
// v1.0.9 W3 Day 14: legal/DMCA — public submission + admin queue
r.setupLegalRoutes(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)
2025-12-03 19:29:37 +00:00
}
2025-12-13 02:34:34 +00:00
return nil
2025-12-03 19:29:37 +00:00
}
// 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)
2026-03-06 09:29:30 +00:00
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(
2026-03-06 09:29:30 +00:00
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)
}
2025-12-03 19:29:37 +00:00
// 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)
2025-12-03 19:29:37 +00:00
userRepo := repositories.NewGormUserRepository(r.db.GormDB)
userService := services.NewUserServiceWithDB(userRepo, r.db.GormDB)
chatHandler := handlers.NewChatHandler(chatService, userService, r.logger)
2026-03-06 09:29:30 +00:00
// 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)
2025-12-03 19:29:37 +00:00
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)
2025-12-03 19:29:37 +00:00
chat.POST("/token", chatHandler.GetToken)
chat.GET("/stats", chatHandler.GetStats) // BE-API-006: Chat stats endpoint
2026-03-06 09:29:30 +00:00
// v0.9.6: Chat rooms REST (reactions, search)
2026-03-06 17:52:08 +00:00
// v0.9.7: Chat room attachments (file upload)
2026-03-06 09:29:30 +00:00
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)
2026-03-06 17:52:08 +00:00
// 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)
}
}
2026-03-06 09:29:30 +00:00
}
2025-12-03 19:29:37 +00:00
}
}
}