Some checks failed
Veza CI / Rust (Stream Server) (push) Successful in 5m26s
Security Scan / Secret Scanning (gitleaks) (push) Failing after 56s
Veza CI / Backend (Go) (push) Failing after 8m39s
Veza CI / Frontend (Web) (push) Failing after 16m22s
Veza CI / Notify on failure (push) Successful in 11s
E2E Playwright / e2e (full) (push) Successful in 20m30s
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>
499 lines
19 KiB
Go
499 lines
19 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
|
|
// 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)
|
|
}
|
|
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)
|
|
|
|
// 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)
|
|
|
|
// 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)
|
|
|
|
// 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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|