package demos import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/omnex/control-plane/api/internal/auth" ) type Handler struct { svc *Service helm *HelmProvisioner // interface déjà implémentée par HelmProvisioner } func NewHandler(svc *Service, helm *HelmProvisioner) *Handler { return &Handler{svc: svc, helm: helm} } type createRequest struct { LeadID string `json:"lead_id" binding:"omitempty,uuid4"` Username string `json:"username" binding:"omitempty,min=3,max=64,alphanum"` // Réglages saisis dans le popup de déploiement (voir ProvisionConfig). TelegramBotUsername string `json:"telegram_bot_username" binding:"omitempty,max=64"` TelegramBotToken string `json:"telegram_bot_token" binding:"omitempty"` NowPaymentsAPIKey string `json:"nowpayments_api_key" binding:"omitempty"` NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret" binding:"omitempty"` StorageDriver string `json:"storage_driver" binding:"omitempty,oneof=local s3"` S3Bucket string `json:"s3_bucket" binding:"omitempty,max=63"` S3Endpoint string `json:"s3_endpoint" binding:"omitempty,max=255"` LBBot1Username string `json:"lb_bot1_username" binding:"omitempty,max=64"` LBBot1Token string `json:"lb_bot1_token" binding:"omitempty"` LBBot2Username string `json:"lb_bot2_username" binding:"omitempty,max=64"` LBBot2Token string `json:"lb_bot2_token" binding:"omitempty"` // LBStrategy : "failover" | "roundrobin" | "leastconn" (vide => failover, // défaut appliqué par lbtelegram lui-même). LBStrategy string `json:"lb_strategy" binding:"omitempty,oneof=failover roundrobin leastconn"` LBJWTTTLSeconds string `json:"lb_jwt_ttl_seconds" binding:"omitempty,numeric"` LBHealthCheckInterval string `json:"lb_health_check_interval" binding:"omitempty,numeric"` // Compte admin de l'application déployée — requis, sans lui personne ne // peut se connecter au backoffice de la démo. AdminUsername string `json:"admin_username" binding:"required,min=3,max=64,alphanum"` AdminPassword string `json:"admin_password" binding:"required,min=8"` } type DetailDemoUserRequest struct { // Namespace identifie la démo de façon unique. Le username ne suffit // pas : un même client peut avoir plusieurs démos (historique, relances), // et interroger par username renvoyait toujours la même (la plus // ancienne), jamais celle réellement sélectionnée dans l'UI. Namespace string `json:"namespace" binding:"required,min=3,max=63"` } type DemoDetailsResponse struct { Namespace string `json:"namespace"` State ResourceState `json:"state"` } // Create : POST /demos — provisionne une démo (202 Accepted, worker asynchrone). func (h *Handler) Create(c *gin.Context) { var req createRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"}) return } cfg := ProvisionConfig{ TelegramBotUsername: req.TelegramBotUsername, TelegramBotToken: req.TelegramBotToken, NowPaymentsAPIKey: req.NowPaymentsAPIKey, NowPaymentsIPNSecret: req.NowPaymentsIPNSecret, StorageDriver: req.StorageDriver, S3Bucket: req.S3Bucket, S3Endpoint: req.S3Endpoint, LBBot1Username: req.LBBot1Username, LBBot1Token: req.LBBot1Token, LBBot2Username: req.LBBot2Username, LBBot2Token: req.LBBot2Token, LBStrategy: req.LBStrategy, LBJWTTTLSeconds: req.LBJWTTTLSeconds, LBHealthCheckInterval: req.LBHealthCheckInterval, AdminUsername: req.AdminUsername, AdminPassword: req.AdminPassword, } d, err := h.svc.Create(req.LeadID, req.Username, cfg) if err != nil { switch { case errors.Is(err, ErrPoolExhausted): c.JSON(http.StatusConflict, gin.H{"error": "plus de ressources externes disponibles pour le moment (slots Telegram/NowPayments épuisés)"}) case errors.Is(err, ErrUserHasActiveDemo): c.JSON(http.StatusConflict, gin.H{"error": "ce client a déjà une démo active"}) case errors.Is(err, ErrS3ConfigIncomplete), errors.Is(err, ErrInvalidStorageDriver): c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"}) } return } c.JSON(http.StatusAccepted, d) } // List : GET /demos. func (h *Handler) List(c *gin.Context) { items, err := h.svc.List() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"}) return } c.JSON(http.StatusOK, gin.H{"items": items}) } // ListMine : GET /demos/mine — démo(s) rattachée(s) au client authentifié. func (h *Handler) ListMine(c *gin.Context) { p := auth.PrincipalFrom(c) if p == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"}) return } items, err := h.svc.ListForUser(p.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"}) return } c.JSON(http.StatusOK, gin.H{"items": items}) } func (h *Handler) ListDetails(c *gin.Context) { p := auth.PrincipalFrom(c) if p == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"}) return } if p.Role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "vous n'avez pas les droits"}) return } var req DetailDemoUserRequest if err := c.ShouldBindJSON(&req); err != nil || req.Namespace == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "namespace requis"}) return } state, err := h.helm.GetResourceState(c.Request.Context(), req.Namespace) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "impossible de récupérer l'état des ressources"}) return } c.JSON(http.StatusOK, DemoDetailsResponse{ Namespace: req.Namespace, State: state, }) } // Get : GET /demos/:id. func (h *Handler) Get(c *gin.Context) { d, err := h.svc.Get(c.Param("id")) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "démo introuvable"}) return } c.JSON(http.StatusOK, d) } // Delete : DELETE /demos/:id — teardown + libération du pool. func (h *Handler) Delete(c *gin.Context) { d, err := h.svc.Delete(c.Param("id")) if err != nil { if errors.Is(err, ErrNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "démo introuvable"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"}) return } c.JSON(http.StatusOK, d) } // Extend : POST /demos/:id/extend — prolonge de 30 jours. func (h *Handler) Extend(c *gin.Context) { d, err := h.svc.Extend(c.Param("id")) if err != nil { switch { case errors.Is(err, ErrNotFound): c.JSON(http.StatusNotFound, gin.H{"error": "démo introuvable"}) case errors.Is(err, ErrNotExtendable): c.JSON(http.StatusConflict, gin.H{"error": "démo non prolongeable"}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"}) } return } c.JSON(http.StatusOK, d) }