fix: multiple error
ci-api / test (push) Successful in 23m32s
ci-web / test (push) Successful in 13m52s

This commit is contained in:
Nuxgrid
2026-08-01 17:24:34 +02:00
parent a06205a608
commit 5d2a132c3c
9 changed files with 144 additions and 54 deletions
+12 -6
View File
@@ -118,12 +118,18 @@ func main() {
}
// Traefik partagé + WAF Coraza : installé une fois pour toutes les démos
// (idempotent). Non-fatal : si ça échoue, l'API démarre quand même, mais
// les démos provisionnées ensuite ne seront ni routées ni protégées tant
// que ce n'est pas corrigé.
if err := helmProv.EnsureSharedInfra(); err != nil {
log.Printf("ATTENTION: infra partagée (Traefik/WAF) indisponible: %v", err)
}
// (idempotent). En arrière-plan et non-fatal : "helm upgrade --install
// --wait --timeout 5m" peut bloquer plusieurs minutes si le cluster est
// lent/injoignable, ce qui retardait le démarrage du serveur HTTP au
// point de faire échouer le healthcheck Docker avant même que l'API
// n'écoute sur le port. Ici ça ne retarde plus rien : les démos
// provisionnées avant la fin de cet appel ne seront juste ni routées ni
// protégées tant qu'il n'a pas fini.
go func() {
if err := helmProv.EnsureSharedInfra(); err != nil {
log.Printf("ATTENTION: infra partagée (Traefik/WAF) indisponible: %v", err)
}
}()
// Service de démos avec le provisioner Helm
demoSvc := demos.NewService(demoStore, demoPool, helmProv, demos.Config{
+30 -16
View File
@@ -24,11 +24,14 @@ type createRequest struct {
}
type DetailDemoUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=64,alphanum"`
// 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 {
Username string `json:"username"`
Namespace string `json:"namespace"`
State ResourceState `json:"state"`
}
@@ -42,11 +45,14 @@ func (h *Handler) Create(c *gin.Context) {
}
d, err := h.svc.Create(req.LeadID, req.Username)
if err != nil {
if errors.Is(err, ErrCapacityReached) {
switch {
case errors.Is(err, ErrCapacityReached):
c.JSON(http.StatusConflict, gin.H{"error": "capacité maximale de démos atteinte"})
return
case errors.Is(err, ErrUserHasActiveDemo):
c.JSON(http.StatusConflict, gin.H{"error": "ce client a déjà une démo active"})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
return
}
c.JSON(http.StatusAccepted, d)
@@ -62,6 +68,21 @@ func (h *Handler) List(c *gin.Context) {
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 {
@@ -74,26 +95,19 @@ func (h *Handler) ListDetails(c *gin.Context) {
}
var req DetailDemoUserRequest
if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "username requis"})
if err := c.ShouldBindJSON(&req); err != nil || req.Namespace == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "namespace requis"})
return
}
demo, err := h.svc.store.GetDemoByUsername(req.Username)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "démo introuvable"})
return
}
state, err := h.helm.GetResourceState(c.Request.Context(), demo.Namespace)
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{
Username: req.Username,
Namespace: demo.Namespace,
Namespace: req.Namespace,
State: state,
})
}
+20 -5
View File
@@ -11,9 +11,10 @@ import (
)
var (
ErrCapacityReached = errors.New("capacité maximale de démos atteinte")
ErrNotFound = errors.New("démo introuvable")
ErrNotExtendable = errors.New("démo non prolongeable dans son état actuel")
ErrCapacityReached = errors.New("capacité maximale de démos atteinte")
ErrNotFound = errors.New("démo introuvable")
ErrNotExtendable = errors.New("démo non prolongeable dans son état actuel")
ErrUserHasActiveDemo = errors.New("ce client a déjà une démo active")
)
// Config du service (injectable pour les tests).
@@ -50,8 +51,22 @@ func NewService(store Store, pool Pool, prov Provisioner, cfg Config) *Service {
}
// Create réserve la capacité + le pool, persiste la démo et déclenche le worker.
// username (optionnel) rattache la démo à un client existant.
// username (optionnel) rattache la démo à un client existant. Un client n'a
// droit qu'à une seule démo active à la fois.
func (s *Service) Create(leadID, username string) (Demo, error) {
normalizedUsername := auth.NormalizeUsername(username)
if normalizedUsername != "" {
existing, err := s.store.ListByUsername(normalizedUsername)
if err != nil {
return Demo{}, err
}
for _, d := range existing {
if d.Status.Active() {
return Demo{}, ErrUserHasActiveDemo
}
}
}
active, err := s.store.CountActive()
if err != nil {
return Demo{}, err
@@ -63,7 +78,7 @@ func (s *Service) Create(leadID, username string) (Demo, error) {
id := uuid.NewString()
demo := Demo{
ID: id,
Username: auth.NormalizeUsername(username),
Username: normalizedUsername,
LeadID: leadID,
Status: StatusPending,
Namespace: "demo-" + shortID(id),
+16 -16
View File
@@ -35,13 +35,13 @@ func (h *Handler) UpdateUsernameById(c *gin.Context) {
return
}
id, ok := c.MustGet("id").(string)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "id invalide"})
p := auth.PrincipalFrom(c)
if p == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
return
}
user, err := h.store.UpdateUsername(id, u.Username)
user, err := h.store.UpdateUsername(p.UserID, u.Username)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "erreur serveur"})
return
@@ -57,9 +57,9 @@ func (h *Handler) UpdatePasswordById(c *gin.Context) {
return
}
id, ok := c.MustGet("id").(string)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "id invalide"})
p := auth.PrincipalFrom(c)
if p == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
return
}
@@ -69,7 +69,7 @@ func (h *Handler) UpdatePasswordById(c *gin.Context) {
return
}
user, err := h.store.UpdatePassword(id, hash)
user, err := h.store.UpdatePassword(p.UserID, hash)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"})
return
@@ -79,12 +79,12 @@ func (h *Handler) UpdatePasswordById(c *gin.Context) {
}
func (h *Handler) GetTelegramById(c *gin.Context) {
id, ok := c.MustGet("id").(string)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "id invalide"})
p := auth.PrincipalFrom(c)
if p == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
return
}
user, err := h.store.GetTelegram(id)
user, err := h.store.GetTelegram(p.UserID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"})
return
@@ -98,12 +98,12 @@ func (h *Handler) SetTelegramById(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"})
return
}
id, ok := c.MustGet("id").(string)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "id invalide"})
p := auth.PrincipalFrom(c)
if p == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
return
}
user, err := h.store.SetTelegram(id, t.Telegram)
user, err := h.store.SetTelegram(p.UserID, t.Telegram)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "erreur serveur"})
return
@@ -63,6 +63,9 @@ func New(d Deps) *gin.Engine {
client.GET("/leads", d.LeadsH.List)
client.PATCH("/leads/:id/status", d.LeadsH.SetStatus)
client.POST("/subscription", d.SubH.AddCode)
if d.DemosH != nil {
client.GET("/demos/mine", d.DemosH.ListMine)
}
}
// Espace admin : provisioning des démos (admin uniquement).
admin := authed.Group("")