2026-02-14 17:04:37 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
2026-02-15 14:55:18 +00:00
|
|
|
"veza-backend-api/internal/config"
|
2026-03-05 22:03:43 +00:00
|
|
|
trackcore "veza-backend-api/internal/core/track"
|
2026-03-09 09:13:18 +00:00
|
|
|
elasticsearch "veza-backend-api/internal/elasticsearch"
|
2026-02-14 17:04:37 +00:00
|
|
|
"veza-backend-api/internal/handlers"
|
|
|
|
|
"veza-backend-api/internal/middleware"
|
|
|
|
|
"veza-backend-api/internal/models"
|
|
|
|
|
"veza-backend-api/internal/repositories"
|
|
|
|
|
"veza-backend-api/internal/services"
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-16 09:17:28 +00:00
|
|
|
// setupValidateRoutes configures the validation endpoint (A01: rate limited)
|
2026-02-14 17:04:37 +00:00
|
|
|
func (r *APIRouter) setupValidateRoutes(router *gin.RouterGroup) {
|
|
|
|
|
validateHandler := handlers.NewValidateHandler(r.logger)
|
2026-02-16 09:17:28 +00:00
|
|
|
validateGroup := router.Group("/")
|
|
|
|
|
if r.config != nil && r.config.EndpointLimiter != nil {
|
|
|
|
|
validateGroup.Use(r.config.EndpointLimiter.ValidateRateLimit())
|
|
|
|
|
}
|
|
|
|
|
validateGroup.POST("validate", validateHandler.Validate)
|
2026-02-14 17:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setupInternalRoutes configure les routes internal (legacy and modern)
|
|
|
|
|
func (r *APIRouter) setupInternalRoutes(router *gin.Engine) {
|
|
|
|
|
uploadDir := r.config.UploadDir
|
|
|
|
|
if uploadDir == "" {
|
|
|
|
|
uploadDir = "uploads/tracks"
|
|
|
|
|
}
|
|
|
|
|
chunksDir := uploadDir + "/chunks"
|
|
|
|
|
|
2026-02-14 21:50:23 +00:00
|
|
|
trackService := trackcore.NewTrackServiceWithDB(r.db, r.logger, uploadDir)
|
2026-02-14 17:04:37 +00:00
|
|
|
if r.config.CacheService != nil {
|
|
|
|
|
trackService.SetCacheService(r.config.CacheService)
|
|
|
|
|
}
|
2026-02-22 16:52:39 +00:00
|
|
|
streamService := services.NewStreamServiceWithAPIKey(r.config.StreamServerURL, r.config.StreamServerInternalAPIKey, r.logger)
|
|
|
|
|
trackService.SetStreamService(streamService) // INT-02: Enable HLS pipeline for regular uploads
|
feat(cdn): Bunny.net signed URLs + HLS cache headers + metric collision fix (W3 Day 13)
CDN edge in front of S3/MinIO via origin-pull. Backend signs URLs
with Bunny.net token-auth (SHA-256 over security_key + path + expires)
so edges verify before serving cached objects ; origin is never hit
on a valid token. Cloudflare CDN / R2 / CloudFront stubs kept.
- internal/services/cdn_service.go : new providers CDNProviderBunny +
CDNProviderCloudflareR2. SecurityKey added to CDNConfig.
generateBunnySignedURL implements the documented Bunny scheme
(url-safe base64, no padding, expires query). HLSSegmentCacheHeaders
+ HLSPlaylistCacheHeaders helpers exported for handlers.
- internal/services/cdn_service_test.go : pin Bunny URL shape +
base64-url charset ; assert empty SecurityKey fails fast (no
silent fallback to unsigned URLs).
- internal/core/track/service.go : new CDNURLSigner interface +
SetCDNService(cdn). GetStorageURL prefers CDN signed URL when
cdnService.IsEnabled, falls back to direct S3 presign on signing
error so a CDN partial outage doesn't block playback.
- internal/api/routes_tracks.go + routes_core.go : wire SetCDNService
on the two TrackService construction sites that serve stream/download.
- internal/config/config.go : 4 new env vars (CDN_ENABLED, CDN_PROVIDER,
CDN_BASE_URL, CDN_SECURITY_KEY). config.CDNService always non-nil
after init ; IsEnabled gates the actual usage.
- internal/handlers/hls_handler.go : segments now return
Cache-Control: public, max-age=86400, immutable (content-addressed
filenames make this safe). Playlists at max-age=60.
- veza-backend-api/.env.template : 4 placeholder env vars.
- docs/ENV_VARIABLES.md §12 : provider matrix + Bunny vs Cloudflare
vs R2 trade-offs.
Bug fix collateral : v1.0.9 Day 11 introduced veza_cache_hits_total
which collided in name with monitoring.CacheHitsTotal (different
label set ⇒ promauto MustRegister panic at process init). Day 13
deletes the monitoring duplicate and restores the metrics-package
counter as the single source of truth (label: subsystem). All 8
affected packages green : services, core/track, handlers, middleware,
websocket/chat, metrics, monitoring, config.
Acceptance (Day 13) : code path is wired ; verifying via real Bunny
edge requires a Pull Zone provisioned by the user (EX-? in roadmap).
On the user side : create Pull Zone w/ origin = MinIO, copy token
auth key into CDN_SECURITY_KEY, set CDN_ENABLED=true.
W3 progress : Redis Sentinel ✓ · MinIO distribué ✓ · CDN ✓ ·
DMCA ⏳ Day 14 · embed ⏳ Day 15.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 12:07:20 +00:00
|
|
|
// v1.0.9 W3 Day 13: wire CDN + S3. SetCDNService is a no-op when CDN_ENABLED=false.
|
|
|
|
|
trackService.SetS3Storage(r.config.S3StorageService, r.config.TrackStorageBackend, r.config.S3Bucket)
|
|
|
|
|
if r.config.CDNService != nil {
|
|
|
|
|
trackService.SetCDNService(r.config.CDNService)
|
|
|
|
|
}
|
2026-02-14 17:04:37 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
trackHandler := trackcore.NewTrackHandler(
|
|
|
|
|
trackService,
|
|
|
|
|
trackUploadService,
|
|
|
|
|
chunkService,
|
|
|
|
|
likeService,
|
|
|
|
|
streamService,
|
|
|
|
|
)
|
|
|
|
|
|
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
|
|
|
streamEventsHandler := handlers.NewStreamEventsHandler(r.logger)
|
|
|
|
|
liveStreamRepo := repositories.NewLiveStreamRepository(r.db.GormDB)
|
feat(v0.703): Go Live & Streaming Complet
- Backend: room creation for live streams, permissions CanJoin/CanSend/CanRead for stream rooms
- LiveViewChat: useLiveStreamChat hook, WebSocket connection, stream_id as room
- LiveViewPlayer: real-time viewer count via polling (5s)
- Media Session: seekbackward/seekforward handlers (10s step)
- GoLiveView.stories.tsx: Default, Loading, Error, StreamKeyVisible
- Docs: API_REFERENCE, CHANGELOG, PROJECT_STATE, FEATURE_STATUS, RETROSPECTIVE_V0703
- SCOPE_CONTROL, .cursorrules: update to v0.801
- Archive V0_703_RELEASE_SCOPE.md
2026-02-25 08:35:22 +00:00
|
|
|
roomRepo := repositories.NewRoomRepository(r.db.GormDB)
|
|
|
|
|
liveStreamService := services.NewLiveStreamService(liveStreamRepo, roomRepo)
|
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
|
|
|
streamEventsHandler.SetLiveStreamService(liveStreamService)
|
|
|
|
|
|
2026-02-14 17:04:37 +00:00
|
|
|
expectedKey := ""
|
|
|
|
|
if r.config != nil {
|
|
|
|
|
expectedKey = r.config.StreamServerInternalAPIKey
|
|
|
|
|
}
|
|
|
|
|
streamCallbackAuth := middleware.StreamCallbackAuth(expectedKey, r.logger)
|
|
|
|
|
|
2026-03-02 18:04:30 +00:00
|
|
|
// v0.941: Removed deprecated /internal/* routes; use /api/v1/internal/* only
|
2026-02-14 17:04:37 +00:00
|
|
|
v1Internal := router.Group("/api/v1/internal")
|
|
|
|
|
v1Internal.Use(streamCallbackAuth)
|
|
|
|
|
{
|
|
|
|
|
v1Internal.POST("/tracks/:id/stream-ready", trackHandler.HandleStreamCallback)
|
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
|
|
|
v1Internal.POST("/stream-events", streamEventsHandler.HandleStreamEvent)
|
2026-02-14 17:04:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setupCorePublicRoutes configure les routes publiques core (health, metrics, upload info)
|
|
|
|
|
func (r *APIRouter) setupCorePublicRoutes(router *gin.Engine) {
|
|
|
|
|
var healthCheckHandler gin.HandlerFunc
|
|
|
|
|
var livenessHandler gin.HandlerFunc
|
|
|
|
|
var readinessHandler gin.HandlerFunc
|
feat(v0.701): AdminTransfers page/route, MSW, stories, Deep Health, API ref, docs, scope v0.702
- Step 13: AdminTransfersPage, LazyAdminTransfers, route /admin/transfers
- Step 14: MSW handlers admin transfers
- Step 15: AdminTransfersView stories (Default, Empty, WithFailedTransfers, Error, Loading)
- Step 16-17: DeepHealth handler (disk, config), GET /health/deep
- Step 19: health_deep_test.go (4 tests)
- Step 20: docs/API_REFERENCE.md
- Step 21: Archive V0_604, MIGRATIONS.md migration 116
- Step 22: CHANGELOG, PROJECT_STATE, FEATURE_STATUS v0.701
- Step 23: RETROSPECTIVE_V0701, V0_702 placeholder, SCOPE_CONTROL, .cursorrules
- Step 24: Archive V0_701_RELEASE_SCOPE
- Fix: AdminTransfersView Select component (use options API)
2026-02-23 22:42:02 +00:00
|
|
|
var deepHealthHandler gin.HandlerFunc
|
2026-02-14 17:04:37 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
var s3Service interface{}
|
|
|
|
|
var jobWorker interface{}
|
|
|
|
|
var emailSender interface{}
|
|
|
|
|
if r.config != nil {
|
|
|
|
|
s3Service = r.config.S3StorageService
|
|
|
|
|
jobWorker = r.config.JobWorker
|
|
|
|
|
emailSender = r.config.EmailSender
|
|
|
|
|
}
|
|
|
|
|
healthHandler := handlers.NewHealthHandlerWithServices(
|
|
|
|
|
r.db.GormDB,
|
|
|
|
|
r.logger,
|
|
|
|
|
redisClient,
|
|
|
|
|
rabbitMQEventBus,
|
|
|
|
|
env,
|
|
|
|
|
s3Service,
|
|
|
|
|
jobWorker,
|
|
|
|
|
emailSender,
|
|
|
|
|
)
|
feat(v0.701): AdminTransfers page/route, MSW, stories, Deep Health, API ref, docs, scope v0.702
- Step 13: AdminTransfersPage, LazyAdminTransfers, route /admin/transfers
- Step 14: MSW handlers admin transfers
- Step 15: AdminTransfersView stories (Default, Empty, WithFailedTransfers, Error, Loading)
- Step 16-17: DeepHealth handler (disk, config), GET /health/deep
- Step 19: health_deep_test.go (4 tests)
- Step 20: docs/API_REFERENCE.md
- Step 21: Archive V0_604, MIGRATIONS.md migration 116
- Step 22: CHANGELOG, PROJECT_STATE, FEATURE_STATUS v0.701
- Step 23: RETROSPECTIVE_V0701, V0_702 placeholder, SCOPE_CONTROL, .cursorrules
- Step 24: Archive V0_701_RELEASE_SCOPE
- Fix: AdminTransfersView Select component (use options API)
2026-02-23 22:42:02 +00:00
|
|
|
if r.config != nil {
|
|
|
|
|
healthHandler.SetDeepHealthConfig(&handlers.DeepHealthConfig{
|
|
|
|
|
JWTSecretSet: len(r.config.JWTSecret) >= 32,
|
|
|
|
|
StripeConnectEnabled: r.config.StripeConnectEnabled,
|
|
|
|
|
PlatformFeeRate: r.config.PlatformFeeRate,
|
|
|
|
|
TransferRetryEnabled: r.config.TransferRetryEnabled,
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-02-14 17:04:37 +00:00
|
|
|
healthCheckHandler = healthHandler.Check
|
|
|
|
|
livenessHandler = healthHandler.Liveness
|
|
|
|
|
readinessHandler = healthHandler.Readiness
|
feat(v0.701): AdminTransfers page/route, MSW, stories, Deep Health, API ref, docs, scope v0.702
- Step 13: AdminTransfersPage, LazyAdminTransfers, route /admin/transfers
- Step 14: MSW handlers admin transfers
- Step 15: AdminTransfersView stories (Default, Empty, WithFailedTransfers, Error, Loading)
- Step 16-17: DeepHealth handler (disk, config), GET /health/deep
- Step 19: health_deep_test.go (4 tests)
- Step 20: docs/API_REFERENCE.md
- Step 21: Archive V0_604, MIGRATIONS.md migration 116
- Step 22: CHANGELOG, PROJECT_STATE, FEATURE_STATUS v0.701
- Step 23: RETROSPECTIVE_V0701, V0_702 placeholder, SCOPE_CONTROL, .cursorrules
- Step 24: Archive V0_701_RELEASE_SCOPE
- Fix: AdminTransfersView Select component (use options API)
2026-02-23 22:42:02 +00:00
|
|
|
deepHealthHandler = healthHandler.DeepHealth
|
2026-02-14 17:04:37 +00:00
|
|
|
} else {
|
|
|
|
|
healthCheckHandler = handlers.SimpleHealthCheck
|
|
|
|
|
livenessHandler = handlers.SimpleHealthCheck
|
|
|
|
|
readinessHandler = handlers.SimpleHealthCheck
|
feat(v0.701): AdminTransfers page/route, MSW, stories, Deep Health, API ref, docs, scope v0.702
- Step 13: AdminTransfersPage, LazyAdminTransfers, route /admin/transfers
- Step 14: MSW handlers admin transfers
- Step 15: AdminTransfersView stories (Default, Empty, WithFailedTransfers, Error, Loading)
- Step 16-17: DeepHealth handler (disk, config), GET /health/deep
- Step 19: health_deep_test.go (4 tests)
- Step 20: docs/API_REFERENCE.md
- Step 21: Archive V0_604, MIGRATIONS.md migration 116
- Step 22: CHANGELOG, PROJECT_STATE, FEATURE_STATUS v0.701
- Step 23: RETROSPECTIVE_V0701, V0_702 placeholder, SCOPE_CONTROL, .cursorrules
- Step 24: Archive V0_701_RELEASE_SCOPE
- Fix: AdminTransfersView Select component (use options API)
2026-02-23 22:42:02 +00:00
|
|
|
deepHealthHandler = func(c *gin.Context) {
|
|
|
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
|
|
|
"success": true,
|
|
|
|
|
"data": gin.H{"status": "unhealthy", "message": "Database not configured"},
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-02-14 17:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deprecationMW := middleware.DeprecationWarning(r.logger)
|
|
|
|
|
healthMonitoringMW := middleware.HealthCheckMonitoring(r.logger, r.monitoringService)
|
2026-03-05 18:27:34 +00:00
|
|
|
metricsProtection := middleware.MetricsProtection(r.logger)
|
2026-02-14 17:04:37 +00:00
|
|
|
|
|
|
|
|
router.GET("/health", deprecationMW, healthMonitoringMW, healthCheckHandler)
|
feat(v0.701): AdminTransfers page/route, MSW, stories, Deep Health, API ref, docs, scope v0.702
- Step 13: AdminTransfersPage, LazyAdminTransfers, route /admin/transfers
- Step 14: MSW handlers admin transfers
- Step 15: AdminTransfersView stories (Default, Empty, WithFailedTransfers, Error, Loading)
- Step 16-17: DeepHealth handler (disk, config), GET /health/deep
- Step 19: health_deep_test.go (4 tests)
- Step 20: docs/API_REFERENCE.md
- Step 21: Archive V0_604, MIGRATIONS.md migration 116
- Step 22: CHANGELOG, PROJECT_STATE, FEATURE_STATUS v0.701
- Step 23: RETROSPECTIVE_V0701, V0_702 placeholder, SCOPE_CONTROL, .cursorrules
- Step 24: Archive V0_701_RELEASE_SCOPE
- Fix: AdminTransfersView Select component (use options API)
2026-02-23 22:42:02 +00:00
|
|
|
router.GET("/health/deep", deprecationMW, healthMonitoringMW, deepHealthHandler)
|
2026-02-14 17:04:37 +00:00
|
|
|
router.GET("/healthz", deprecationMW, healthMonitoringMW, livenessHandler)
|
|
|
|
|
router.GET("/readyz", deprecationMW, healthMonitoringMW, readinessHandler)
|
2026-03-05 18:27:34 +00:00
|
|
|
router.GET("/metrics", deprecationMW, metricsProtection, handlers.PrometheusMetrics())
|
2026-02-14 17:04:37 +00:00
|
|
|
if r.config != nil && r.config.ErrorMetrics != nil {
|
2026-03-05 18:27:34 +00:00
|
|
|
router.GET("/metrics/aggregated", deprecationMW, metricsProtection, handlers.AggregatedMetrics(r.config.ErrorMetrics))
|
2026-02-14 17:04:37 +00:00
|
|
|
}
|
2026-03-05 18:27:34 +00:00
|
|
|
router.GET("/system/metrics", deprecationMW, metricsProtection, handlers.SystemMetrics)
|
2026-02-14 17:04:37 +00:00
|
|
|
|
|
|
|
|
v1Public := router.Group("/api/v1")
|
|
|
|
|
{
|
|
|
|
|
v1Public.GET("/health", healthCheckHandler)
|
feat(v0.701): AdminTransfers page/route, MSW, stories, Deep Health, API ref, docs, scope v0.702
- Step 13: AdminTransfersPage, LazyAdminTransfers, route /admin/transfers
- Step 14: MSW handlers admin transfers
- Step 15: AdminTransfersView stories (Default, Empty, WithFailedTransfers, Error, Loading)
- Step 16-17: DeepHealth handler (disk, config), GET /health/deep
- Step 19: health_deep_test.go (4 tests)
- Step 20: docs/API_REFERENCE.md
- Step 21: Archive V0_604, MIGRATIONS.md migration 116
- Step 22: CHANGELOG, PROJECT_STATE, FEATURE_STATUS v0.701
- Step 23: RETROSPECTIVE_V0701, V0_702 placeholder, SCOPE_CONTROL, .cursorrules
- Step 24: Archive V0_701_RELEASE_SCOPE
- Fix: AdminTransfersView Select component (use options API)
2026-02-23 22:42:02 +00:00
|
|
|
v1Public.GET("/health/deep", deepHealthHandler)
|
2026-02-14 17:04:37 +00:00
|
|
|
v1Public.GET("/healthz", livenessHandler)
|
|
|
|
|
v1Public.GET("/readyz", readinessHandler)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 18:27:34 +00:00
|
|
|
v1Public.GET("/metrics", metricsProtection, handlers.PrometheusMetrics())
|
2026-02-14 17:04:37 +00:00
|
|
|
if r.config != nil && r.config.ErrorMetrics != nil {
|
2026-03-05 18:27:34 +00:00
|
|
|
v1Public.GET("/metrics/aggregated", metricsProtection, handlers.AggregatedMetrics(r.config.ErrorMetrics))
|
2026-02-14 17:04:37 +00:00
|
|
|
}
|
2026-03-05 18:27:34 +00:00
|
|
|
v1Public.GET("/system/metrics", metricsProtection, handlers.SystemMetrics)
|
2026-02-14 17:04:37 +00:00
|
|
|
|
2026-02-14 17:05:11 +00:00
|
|
|
if r.db != nil && r.db.GormDB != nil && r.config != nil {
|
2026-02-14 17:04:37 +00:00
|
|
|
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))
|
|
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if r.config != nil {
|
|
|
|
|
frontendLogHandler, err := handlers.NewFrontendLogHandler(r.config, r.logger)
|
|
|
|
|
if err != nil {
|
|
|
|
|
r.logger.Warn("Failed to create frontend log handler, frontend logs will not be stored", zap.Error(err))
|
|
|
|
|
} else {
|
2026-02-14 19:19:56 +00:00
|
|
|
logsRateLimit := middleware.FrontendLogRateLimit(r.config.RedisClient)
|
|
|
|
|
v1Public.POST("/logs/frontend", logsRateLimit, frontendLogHandler.ReceiveLog)
|
2026-02-14 17:04:37 +00:00
|
|
|
r.logger.Info("Frontend logging endpoint enabled", zap.String("endpoint", "/api/v1/logs/frontend"))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-25 18:55:21 +00:00
|
|
|
|
|
|
|
|
// v0.803 ADM1-04: Active announcements (public)
|
|
|
|
|
if r.db != nil && r.db.GormDB != nil {
|
|
|
|
|
announcementSvc := services.NewAnnouncementService(r.db.GormDB, r.logger)
|
|
|
|
|
announcementHandler := handlers.NewAnnouncementHandler(announcementSvc)
|
|
|
|
|
v1Public.GET("/announcements/active", announcementHandler.GetActive)
|
|
|
|
|
}
|
feat(webrtc): coturn ICE config endpoint + frontend wiring + ops template (v1.0.9 item 1.2)
Closes FUNCTIONAL_AUDIT.md §4 #1: WebRTC 1:1 calls had working
signaling but no NAT traversal, so calls between two peers behind
symmetric NAT (corporate firewalls, mobile carrier CGNAT, Incus
container default networking) failed silently after the SDP exchange.
Backend:
- GET /api/v1/config/webrtc (public) returns {iceServers: [...]}
built from WEBRTC_STUN_URLS / WEBRTC_TURN_URLS / *_USERNAME /
*_CREDENTIAL env vars. Half-config (URLs without creds, or vice
versa) deliberately omits the TURN block — a half-configured TURN
surfaces auth errors at call time instead of falling back cleanly
to STUN-only.
- 4 handler tests cover the matrix.
Frontend:
- services/api/webrtcConfig.ts caches the config for the page
lifetime and falls back to the historical hardcoded Google STUN
if the fetch fails.
- useWebRTC fetches at mount, hands iceServers synchronously to
every RTCPeerConnection, exposes a {hasTurn, loaded} hint.
- CallButton tooltip warns up-front when TURN isn't configured
instead of letting calls time out silently.
Ops:
- infra/coturn/turnserver.conf — annotated template with the SSRF-
safe denied-peer-ip ranges, prometheus exporter, TLS for TURNS,
static lt-cred-mech (REST-secret rotation deferred to v1.1).
- infra/coturn/README.md — Incus deploy walkthrough, smoke test
via turnutils_uclient, capacity rules of thumb.
- docs/ENV_VARIABLES.md gains a 13bis. WebRTC ICE servers section.
Coturn deployment itself is a separate ops action — this commit lands
the plumbing so the deploy can light up the path with zero code
changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:38:42 +00:00
|
|
|
|
|
|
|
|
// v1.0.9 item 1.2 — WebRTC ICE servers for the SPA. Public so the
|
|
|
|
|
// frontend can fetch it before the user is authenticated (the call
|
|
|
|
|
// surface lives in the chat tab, but the STUN/TURN bootstrap is
|
|
|
|
|
// page-load-time, not call-time, to minimise latency on the first
|
|
|
|
|
// call attempt).
|
|
|
|
|
if r.config != nil {
|
|
|
|
|
v1Public.GET("/config/webrtc", handlers.GetWebRTCConfig(r.config))
|
|
|
|
|
}
|
2026-02-14 17:04:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
csrfMiddleware := middleware.NewCSRFMiddleware(r.config.RedisClient, r.logger)
|
|
|
|
|
csrfMiddleware.SetEnvironment(r.config.Env)
|
|
|
|
|
csrfHandler := handlers.NewCSRFHandler(csrfMiddleware, r.logger)
|
|
|
|
|
if r.config.AuthMiddleware != nil {
|
|
|
|
|
v1.GET("/csrf-token", r.config.AuthMiddleware.OptionalAuth(), csrfHandler.GetCSRFToken())
|
|
|
|
|
} else {
|
|
|
|
|
v1.GET("/csrf-token", csrfHandler.GetCSRFToken())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected := v1.Group("/")
|
|
|
|
|
if r.config.AuthMiddleware != nil {
|
|
|
|
|
protected.Use(r.config.AuthMiddleware.RequireAuth())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sessionService := services.NewSessionService(r.db, r.logger)
|
|
|
|
|
|
|
|
|
|
if r.config.RedisClient != nil {
|
|
|
|
|
protected.Use(csrfMiddleware.Middleware())
|
|
|
|
|
r.logger.Info("CSRF protection enabled for core protected routes",
|
|
|
|
|
zap.String("environment", r.config.Env),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
r.logger.Warn("Redis not available - CSRF protection disabled (non-production environment)",
|
|
|
|
|
zap.String("environment", r.config.Env),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
uploadConfig.ClamAVEnabled = false
|
|
|
|
|
uploadValidator, _ = services.NewUploadValidator(uploadConfig, r.logger)
|
|
|
|
|
}
|
|
|
|
|
auditService := services.NewAuditService(r.db, r.logger)
|
|
|
|
|
trackUploadService := services.NewTrackUploadService(r.db.GormDB, r.logger)
|
|
|
|
|
|
|
|
|
|
sessionHandler := handlers.NewSessionHandler(sessionService, auditService, r.logger)
|
|
|
|
|
uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, trackUploadService, r.logger, r.config.MaxConcurrentUploads)
|
|
|
|
|
auditHandler := handlers.NewAuditHandler(auditService, r.logger)
|
|
|
|
|
|
|
|
|
|
sessions := protected.Group("/sessions")
|
|
|
|
|
{
|
|
|
|
|
sessions.POST("/logout", sessionHandler.Logout())
|
|
|
|
|
sessions.POST("/logout-all", sessionHandler.LogoutAll())
|
2026-02-20 13:52:20 +00:00
|
|
|
sessions.POST("/logout-others", sessionHandler.LogoutOthers())
|
2026-02-14 17:04:37 +00:00
|
|
|
sessions.GET("", sessionHandler.GetSessions())
|
|
|
|
|
sessions.GET("/", sessionHandler.GetSessions())
|
|
|
|
|
sessions.DELETE("/:session_id", sessionHandler.RevokeSession())
|
|
|
|
|
sessions.GET("/stats", sessionHandler.GetSessionStats())
|
|
|
|
|
sessions.POST("/refresh", sessionHandler.RefreshSession())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
feat: backend, stream server & infra improvements
Backend (Go):
- Config: CORS, RabbitMQ, rate limit, main config updates
- Routes: core, distribution, tracks routing changes
- Middleware: rate limiter, endpoint limiter, response cache hardening
- Handlers: distribution, search handler fixes
- Workers: job worker improvements
- Upload validator and logging config additions
- New migrations: products, orders, performance indexes
- Seed tooling and data
Stream Server (Rust):
- Audio processing, config, routes, simple stream server updates
- Dockerfile improvements
Infrastructure:
- docker-compose.yml updates
- nginx-rtmp config changes
- Makefile improvements (config, dev, high, infra)
- Root package.json and lock file updates
- .env.example updates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:36:06 +00:00
|
|
|
// v0.803 ADM1: User report endpoint — moved to routes_moderation.go (F412 enhanced reporting)
|
|
|
|
|
// reportServiceForUser := services.NewReportService(r.db.GormDB, r.logger)
|
|
|
|
|
// reportHandlerForUser := handlers.NewReportHandler(reportServiceForUser)
|
2026-02-25 18:53:04 +00:00
|
|
|
|
2026-03-02 18:25:37 +00:00
|
|
|
// v0.971: Client-visible feature flags (e.g. WEBRTC_CALLS for CallButton)
|
|
|
|
|
featureFlagSvc := services.NewFeatureFlagService(r.db.GormDB, r.logger)
|
|
|
|
|
featureFlagHandler := handlers.NewFeatureFlagHandler(featureFlagSvc)
|
|
|
|
|
protected.GET("/feature-flags", featureFlagHandler.List)
|
|
|
|
|
|
2026-02-14 17:04:37 +00:00
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uploadDir := r.config.UploadDir
|
|
|
|
|
if uploadDir == "" {
|
|
|
|
|
uploadDir = "uploads/tracks"
|
|
|
|
|
}
|
2026-02-14 21:50:23 +00:00
|
|
|
trackServiceForDashboard := trackcore.NewTrackServiceWithDB(r.db, r.logger, uploadDir)
|
2026-02-14 17:04:37 +00:00
|
|
|
if r.config.CacheService != nil {
|
|
|
|
|
trackServiceForDashboard.SetCacheService(r.config.CacheService)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackListFunc := func(ctx context.Context, params handlers.TrackListParamsForDashboard) ([]*models.Track, int64, error) {
|
|
|
|
|
return trackServiceForDashboard.ListTracks(ctx, trackcore.TrackListParams{
|
|
|
|
|
Page: params.Page,
|
|
|
|
|
Limit: params.Limit,
|
|
|
|
|
UserID: params.UserID,
|
|
|
|
|
Genre: params.Genre,
|
|
|
|
|
Format: params.Format,
|
|
|
|
|
SortBy: params.SortBy,
|
|
|
|
|
SortOrder: params.SortOrder,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dashboardHandler := handlers.NewDashboardHandler(auditService, trackListFunc, r.logger)
|
|
|
|
|
protected.GET("/dashboard", dashboardHandler.GetDashboard())
|
|
|
|
|
|
|
|
|
|
roomRepo := repositories.NewRoomRepository(r.db.GormDB)
|
|
|
|
|
messageRepo := repositories.NewChatMessageRepository(r.db.GormDB)
|
|
|
|
|
roomService := services.NewRoomService(roomRepo, messageRepo, r.logger)
|
|
|
|
|
roomHandler := handlers.NewRoomHandler(roomService, r.logger)
|
|
|
|
|
|
|
|
|
|
conversations := protected.Group("/conversations")
|
|
|
|
|
{
|
|
|
|
|
conversations.GET("", roomHandler.GetUserRooms)
|
|
|
|
|
conversations.POST("", roomHandler.CreateRoom)
|
2026-03-06 17:52:08 +00:00
|
|
|
conversations.GET("/join/:token", roomHandler.JoinByToken)
|
2026-02-14 17:04:37 +00:00
|
|
|
conversations.GET("/:id", roomHandler.GetRoom)
|
|
|
|
|
conversations.PUT("/:id", roomHandler.UpdateRoom)
|
|
|
|
|
conversations.DELETE("/:id", roomHandler.DeleteRoom)
|
2026-03-06 17:58:37 +00:00
|
|
|
conversations.POST("/:id/leave", roomHandler.LeaveRoom) // v0.9.7 self-leave
|
2026-03-06 09:29:30 +00:00
|
|
|
conversations.GET("/:id/members", roomHandler.GetMembers)
|
2026-02-14 17:04:37 +00:00
|
|
|
conversations.POST("/:id/members", roomHandler.AddMember)
|
2026-03-06 17:52:08 +00:00
|
|
|
conversations.DELETE("/:id/members/:userId", roomHandler.KickMember)
|
2026-03-06 17:58:37 +00:00
|
|
|
conversations.PATCH("/:id/members/:userId", roomHandler.UpdateMemberRole) // v0.9.7
|
2026-02-14 17:04:37 +00:00
|
|
|
conversations.POST("/:id/participants", roomHandler.AddParticipant)
|
|
|
|
|
conversations.DELETE("/:id/participants/:userId", roomHandler.RemoveParticipant)
|
2026-03-06 17:52:08 +00:00
|
|
|
conversations.POST("/:id/invitations", roomHandler.CreateInvitation)
|
2026-02-14 17:04:37 +00:00
|
|
|
conversations.GET("/:id/history", roomHandler.GetRoomHistory)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 15:41:39 +00:00
|
|
|
r.notificationService = services.NewNotificationService(r.db, r.logger)
|
|
|
|
|
vapidPublic := os.Getenv("VAPID_PUBLIC_KEY")
|
|
|
|
|
vapidPrivate := os.Getenv("VAPID_PRIVATE_KEY")
|
|
|
|
|
r.pushService = services.NewPushService(r.db.GormDB, r.logger, vapidPublic, vapidPrivate)
|
|
|
|
|
r.notificationService.SetPushService(r.pushService)
|
|
|
|
|
handlers.NewNotificationHandlers(r.notificationService, r.pushService)
|
2026-02-14 17:04:37 +00:00
|
|
|
notifications := protected.Group("/notifications")
|
|
|
|
|
{
|
|
|
|
|
notifications.GET("", handlers.NotificationHandlersInstance.GetNotifications)
|
|
|
|
|
notifications.GET("/unread-count", handlers.NotificationHandlersInstance.GetUnreadCount)
|
2026-02-21 15:41:39 +00:00
|
|
|
notifications.GET("/preferences", handlers.NotificationHandlersInstance.GetPreferences)
|
|
|
|
|
notifications.PUT("/preferences", handlers.NotificationHandlersInstance.UpdatePreferences)
|
|
|
|
|
notifications.POST("/push/subscribe", handlers.NotificationHandlersInstance.SubscribePush)
|
2026-02-14 17:04:37 +00:00
|
|
|
notifications.POST("/:id/read", handlers.NotificationHandlersInstance.MarkAsRead)
|
|
|
|
|
notifications.POST("/read-all", handlers.NotificationHandlersInstance.MarkAllAsRead)
|
|
|
|
|
notifications.DELETE("/:id", handlers.NotificationHandlersInstance.DeleteNotification)
|
|
|
|
|
notifications.DELETE("", handlers.NotificationHandlersInstance.DeleteAllNotifications)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
admin := v1.Group("/admin")
|
|
|
|
|
{
|
|
|
|
|
if r.config.AuthMiddleware != nil {
|
|
|
|
|
admin.Use(r.config.AuthMiddleware.RequireAuth())
|
|
|
|
|
admin.Use(r.config.AuthMiddleware.RequireAdmin())
|
feat(v0.12.6.2): enforce MFA for admin/moderator + align refresh token TTL to 7 days
TASK-SFIX-001: MFA enforcement for privileged roles
- Add RequireMFA() middleware, TwoFactorChecker interface, SetTwoFactorChecker()
- Apply to all 3 admin route groups (platform, moderation, core)
- Returns 403 "mfa_setup_required" if admin/moderator without 2FA
- Regular users bypass the check
- Ref: ORIGIN_SECURITY_FRAMEWORK.md Rule 5
TASK-SFIX-002: Refresh token TTL alignment
- jwt_service.go: RefreshTokenTTL 14d→7d, RememberMeRefreshTokenTTL 30d→7d
- handlers/auth.go: Cookie max-age and session expiresIn → 7d across
Login, LoginWith2FA, Register, Refresh handlers
- middleware/auth.go: Session auto-refresh default 30d→7d
- Ref: ORIGIN_SECURITY_FRAMEWORK.md Rule 4
TASK-SFIX-003: 5 unit tests — all PASS
- TestRequireMFA_AdminWithoutMFA, TestRequireMFA_AdminWithMFA
- TestRequireMFA_RegularUserNotAffected
- TestRefreshTokenTTL_Is7Days, TestAccessTokenTTL_Is5Minutes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 05:53:27 +00:00
|
|
|
admin.Use(r.config.AuthMiddleware.RequireMFA()) // SFIX-001: MFA obligatoire pour admin
|
2026-02-14 17:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
admin.GET("/audit/logs", auditHandler.SearchLogs())
|
|
|
|
|
admin.GET("/audit/stats", auditHandler.GetStats())
|
|
|
|
|
admin.GET("/audit/suspicious", auditHandler.DetectSuspiciousActivity())
|
|
|
|
|
|
2026-02-25 18:53:04 +00:00
|
|
|
// v0.803 ADM1: Moderation queue
|
|
|
|
|
reportService := services.NewReportService(r.db.GormDB, r.logger)
|
|
|
|
|
reportHandler := handlers.NewReportHandler(reportService)
|
|
|
|
|
admin.GET("/reports", reportHandler.ListReports)
|
|
|
|
|
admin.POST("/reports/:id/resolve", reportHandler.ResolveReport)
|
|
|
|
|
|
2026-04-16 12:57:06 +00:00
|
|
|
// v0.803 ADM1-03: Maintenance mode toggle — v1.0.4: persisted via
|
|
|
|
|
// platform_settings so a toggle on one pod affects every other pod.
|
2026-02-25 18:54:22 +00:00
|
|
|
admin.PUT("/maintenance", func(c *gin.Context) {
|
|
|
|
|
var req struct {
|
|
|
|
|
Enabled bool `json:"enabled"`
|
|
|
|
|
}
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "enabled is required"})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-04-16 12:57:06 +00:00
|
|
|
if r.db != nil && r.db.GormDB != nil {
|
|
|
|
|
if err := r.db.GormDB.WithContext(c.Request.Context()).Exec(
|
|
|
|
|
`INSERT INTO platform_settings (key, value_bool, description)
|
|
|
|
|
VALUES ('maintenance_mode', ?, 'When TRUE, all API requests outside the exempt list return 503.')
|
|
|
|
|
ON CONFLICT (key) DO UPDATE SET value_bool = EXCLUDED.value_bool, updated_at = NOW()`,
|
|
|
|
|
req.Enabled,
|
|
|
|
|
).Error; err != nil {
|
|
|
|
|
r.logger.Error("Failed to persist maintenance flag",
|
|
|
|
|
zap.Bool("enabled", req.Enabled),
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
)
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to persist maintenance flag"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-25 18:54:22 +00:00
|
|
|
middleware.SetMaintenanceMode(req.Enabled)
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"maintenance_mode": req.Enabled})
|
|
|
|
|
})
|
|
|
|
|
admin.GET("/maintenance", func(c *gin.Context) {
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"maintenance_mode": middleware.MaintenanceModeEnabled()})
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-25 18:55:21 +00:00
|
|
|
// v0.803 ADM1-04: Announcements CRUD
|
|
|
|
|
announcementSvc := services.NewAnnouncementService(r.db.GormDB, r.logger)
|
|
|
|
|
announcementHandler := handlers.NewAnnouncementHandler(announcementSvc)
|
|
|
|
|
admin.GET("/announcements", announcementHandler.List)
|
|
|
|
|
admin.POST("/announcements", announcementHandler.Create)
|
|
|
|
|
admin.DELETE("/announcements/:id", announcementHandler.Delete)
|
|
|
|
|
|
2026-02-25 18:56:24 +00:00
|
|
|
// v0.803 ADM1-05: Feature flags CRUD
|
|
|
|
|
featureFlagSvc := services.NewFeatureFlagService(r.db.GormDB, r.logger)
|
|
|
|
|
featureFlagHandler := handlers.NewFeatureFlagHandler(featureFlagSvc)
|
|
|
|
|
admin.GET("/feature-flags", featureFlagHandler.List)
|
|
|
|
|
admin.PUT("/feature-flags/:name", featureFlagHandler.Toggle)
|
|
|
|
|
|
2026-02-23 22:33:54 +00:00
|
|
|
// v0.701: Admin Transfer Dashboard
|
|
|
|
|
var adminTransferHandler *handlers.AdminTransferHandler
|
|
|
|
|
if r.config != nil && r.config.StripeConnectEnabled && r.config.StripeConnectSecretKey != "" {
|
|
|
|
|
stripeConnectSvc := services.NewStripeConnectService(r.db.GormDB, r.config.StripeConnectSecretKey, r.logger)
|
|
|
|
|
adminTransferHandler = handlers.NewAdminTransferHandler(r.db.GormDB, stripeConnectSvc, r.config.PlatformFeeRate, r.logger)
|
|
|
|
|
} else {
|
|
|
|
|
feeRate := 0.10
|
|
|
|
|
if r.config != nil {
|
|
|
|
|
feeRate = r.config.PlatformFeeRate
|
|
|
|
|
}
|
|
|
|
|
adminTransferHandler = handlers.NewAdminTransferHandler(r.db.GormDB, nil, feeRate, r.logger)
|
|
|
|
|
}
|
|
|
|
|
admin.GET("/transfers", adminTransferHandler.GetTransfers)
|
|
|
|
|
admin.POST("/transfers/:id/retry", adminTransferHandler.RetryTransfer)
|
|
|
|
|
|
2026-02-15 14:55:18 +00:00
|
|
|
// P1.5: pprof endpoints disabled in production to avoid leaking sensitive runtime info
|
|
|
|
|
if r.config != nil && r.config.Env != config.EnvProduction && r.config.Env != "prod" {
|
|
|
|
|
admin.Any("/debug/pprof/*path", gin.WrapH(http.DefaultServeMux))
|
|
|
|
|
r.logger.Info("pprof endpoints enabled at /api/v1/admin/debug/pprof/")
|
|
|
|
|
}
|
2026-02-14 17:04:37 +00:00
|
|
|
|
|
|
|
|
if r.authService != nil {
|
|
|
|
|
admin.POST("/auth/unlock-account", handlers.UnlockAccount(r.authService, r.logger))
|
|
|
|
|
}
|
2026-03-09 09:13:18 +00:00
|
|
|
|
|
|
|
|
// v0.10.2 F361: Elasticsearch reindex (admin only)
|
|
|
|
|
admin.POST("/search/reindex", func(c *gin.Context) {
|
|
|
|
|
esCfg := elasticsearch.LoadConfig()
|
|
|
|
|
if !esCfg.Enabled {
|
|
|
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Elasticsearch not configured"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
esClient, err := elasticsearch.NewClient(esCfg, r.logger)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Elasticsearch unavailable", "detail": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
idx := elasticsearch.NewIndexer(esClient, r.db.GormDB, r.logger)
|
|
|
|
|
if err := idx.ReindexAll(c.Request.Context()); err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Reindex failed", "detail": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "message": "Reindex completed"})
|
|
|
|
|
})
|
2026-02-14 17:04:37 +00:00
|
|
|
}
|
|
|
|
|
}
|