127 lines
4.8 KiB
Go
127 lines
4.8 KiB
Go
// 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/projects"
|
|
"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
|
|
ProjectsH *projects.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)
|
|
}
|
|
if d.ProjectsH != nil {
|
|
admin.POST("/projects", d.ProjectsH.Create)
|
|
admin.GET("/projects", d.ProjectsH.List)
|
|
admin.GET("/projects/:id", d.ProjectsH.Get)
|
|
admin.DELETE("/projects/:id", d.ProjectsH.Delete)
|
|
admin.POST("/projects/:id/extend", d.ProjectsH.Extend)
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|