// Package router assemble les routes de l'API Omnex. package router import ( "net/http" "github.com/gin-gonic/gin" "github.com/omnex/control-plane/api/internal/auth" "github.com/omnex/control-plane/api/internal/config" "github.com/omnex/control-plane/api/internal/demos" "github.com/omnex/control-plane/api/internal/downloads" "github.com/omnex/control-plane/api/internal/httpx" "github.com/omnex/control-plane/api/internal/profile" "github.com/omnex/control-plane/api/internal/session" "github.com/omnex/control-plane/api/internal/sub" ) // Deps : dépendances injectées (facilite les tests). type Deps struct { Cfg config.Config Issuer *auth.Issuer Sessions session.Manager AuthH *auth.Handler DemosH *demos.Handler SubH *sub.Handler ProfileH *profile.Handler DownloadsH *downloads.Handler } // New construit l'engine Gin avec toute la chaîne de sécurité. func New(d Deps) *gin.Engine { if d.Cfg.Env == "prod" { gin.SetMode(gin.ReleaseMode) } r := gin.New() // Sans ça, Gin fait confiance par défaut au X-Forwarded-For fourni par // N'IMPORTE QUEL client pour déterminer c.ClientIP() (utilisé par // RateLimit) — un attaquant peut alors faire croire que chaque requête // vient d'une IP différente en changeant juste cet en-tête, contournant // intégralement la limitation de débit sur /auth/login et // /auth/register (trouvé en pentest, voir F-005). // // L'API n'est jamais exposée directement (pas de "ports:" dans // docker-compose.yml, voir docker/docker-compose.yml) — seul nginx/waf, // sur le réseau Docker interne, peut l'atteindre (proxy_pass vers // http://api:8080, voir docker/waf/nginx.conf qui construit le // X-Forwarded-For via $proxy_add_x_forwarded_for : ajoute toujours la // vraie IP vue par nginx en dernière position, sans jamais écraser une // valeur fournie par le client). En ne faisant confiance qu'aux plages // privées RFC1918 (réseau Docker interne), Gin ignore la partie du // X-Forwarded-For contrôlée par le client et ne retient que la partie // ajoutée par nginx. if err := r.SetTrustedProxies([]string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}); err != nil { // CIDR statiques et valides : ne peut arriver qu'en cas d'erreur de // programmation (typo) — fatal au démarrage plutôt que de tourner // avec la protection anti-spoofing désactivée sans s'en rendre compte. panic(err) } r.Use(gin.Recovery()) r.Use(httpx.SecurityHeaders()) r.Use(httpx.CORS(d.Cfg.AllowedOrigins)) r.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) api := r.Group("/api/v1") // Public : login, inscription, rate-limités. api.POST("/auth/login", httpx.RateLimit(1, 5), d.AuthH.Login) api.POST("/auth/register", httpx.RateLimit(0.2, 3), d.AuthH.Register) // Toute route authentifiée (session valide, n'importe quel rôle). authed := api.Group("") authed.Use(auth.RequireAuth(d.Issuer, d.Sessions)) { authed.POST("/auth/logout", d.AuthH.Logout) authed.GET("/auth/me", d.AuthH.Me) authed.POST("/profile/username", d.ProfileH.UpdateUsernameById) authed.POST("/profile/password", d.ProfileH.UpdatePasswordById) authed.GET("/profile/telegram", d.ProfileH.GetTelegramById) authed.POST("/profile/telegram", d.ProfileH.SetTelegramById) } client := authed.Group("") client.Use(auth.RequireRole(auth.RoleClient)) { client.POST("/subscription", d.SubH.AddCode) if d.DemosH != nil { client.GET("/demos/mine", d.DemosH.ListMine) } if d.DownloadsH != nil { client.GET("/apps", d.DownloadsH.List) client.GET("/apps/:name", d.DownloadsH.Download) } } // Espace admin : provisioning des démos (admin uniquement). admin := authed.Group("") admin.Use(auth.RequireRole(auth.RoleAdmin)) { admin.GET("/codes", d.SubH.ListCodes) admin.POST("/codes", d.SubH.CreateCodeForBuy) admin.GET("/profile/alerts", d.ProfileH.GetAlerts) admin.POST("/profile/alerts", d.ProfileH.SetAlerts) admin.POST("/profile/alerts/test", d.ProfileH.TestAlerts) admin.GET("/premium", d.SubH.GetPremiumUser) if d.DemosH != nil { admin.POST("/demos", d.DemosH.Create) admin.GET("/demos", d.DemosH.List) admin.GET("/demos/:id", d.DemosH.Get) admin.DELETE("/demos/:id", d.DemosH.Delete) admin.POST("/demos/:id/extend", d.DemosH.Extend) admin.POST("/demos/:id/domain", d.DemosH.SetDomain) admin.POST("/demos/:id/premium", d.DemosH.TransferToPremium) admin.POST("/demos/details", d.DemosH.ListDetails) } } return r }