chore: update
This commit is contained in:
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/omnex/control-plane/api/internal/auth"
|
||||
"github.com/omnex/control-plane/api/internal/demos"
|
||||
"github.com/omnex/control-plane/api/internal/leads"
|
||||
"github.com/omnex/control-plane/api/internal/sub"
|
||||
)
|
||||
|
||||
@@ -41,7 +40,6 @@ func AutoMigrate(gdb *gorm.DB) error {
|
||||
return gdb.AutoMigrate(
|
||||
&auth.User{},
|
||||
&sub.CodeBuySub{},
|
||||
&leads.Lead{},
|
||||
&demos.Demo{},
|
||||
&demos.ExternalResource{},
|
||||
)
|
||||
|
||||
@@ -19,7 +19,6 @@ func NewHandler(svc *Service, helm *HelmProvisioner) *Handler {
|
||||
}
|
||||
|
||||
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).
|
||||
@@ -85,7 +84,7 @@ func (h *Handler) Create(c *gin.Context) {
|
||||
AdminUsername: req.AdminUsername,
|
||||
AdminPassword: req.AdminPassword,
|
||||
}
|
||||
d, err := h.svc.Create(req.LeadID, req.Username, cfg)
|
||||
d, err := h.svc.Create(req.Username, cfg)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrPoolExhausted):
|
||||
|
||||
@@ -5,7 +5,6 @@ import "time"
|
||||
type Demo struct {
|
||||
ID string `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
Username string `gorm:"type:varchar(64);index" json:"username,omitempty"`
|
||||
LeadID string `gorm:"type:varchar(36);index" json:"lead_id,omitempty"`
|
||||
Status Status `gorm:"size:20;not null;index" json:"status"`
|
||||
Namespace string `gorm:"size:63;uniqueIndex" json:"namespace"`
|
||||
URL string `gorm:"size:255" json:"url"`
|
||||
|
||||
@@ -49,7 +49,7 @@ func NewService(store Store, pool Pool, prov Provisioner, cfg Config) *Service {
|
||||
// username (optionnel) rattache la démo à un client existant. Un client n'a
|
||||
// droit qu'à une seule démo active à la fois. cfg (bot Telegram, stockage)
|
||||
// n'est jamais persisté : transmis tel quel au provisioning.
|
||||
func (s *Service) Create(leadID, username string, cfg ProvisionConfig) (Demo, error) {
|
||||
func (s *Service) Create(username string, cfg ProvisionConfig) (Demo, error) {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Demo{}, err
|
||||
}
|
||||
@@ -70,7 +70,6 @@ func (s *Service) Create(leadID, username string, cfg ProvisionConfig) (Demo, er
|
||||
demo := Demo{
|
||||
ID: id,
|
||||
Username: normalizedUsername,
|
||||
LeadID: leadID,
|
||||
Status: StatusPending,
|
||||
Namespace: "demo-" + shortID(id),
|
||||
CreatedAt: s.now().UTC(),
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package leads
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormStore : Store adossé à PostgreSQL via GORM.
|
||||
type GormStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewGormStore(db *gorm.DB) *GormStore {
|
||||
return &GormStore{db: db}
|
||||
}
|
||||
|
||||
func (s *GormStore) Create(l Lead) (Lead, error) {
|
||||
l.ID = uuid.NewString()
|
||||
l.Status = StatusNew
|
||||
l.CreatedAt = time.Now().UTC()
|
||||
if err := s.db.Create(&l).Error; err != nil {
|
||||
return Lead{}, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (s *GormStore) List() ([]Lead, error) {
|
||||
var out []Lead
|
||||
if err := s.db.Order("created_at DESC").Find(&out).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *GormStore) Get(id string) (Lead, bool) {
|
||||
var l Lead
|
||||
err := s.db.Where("id = ?", id).First(&l).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) || err != nil {
|
||||
return Lead{}, false
|
||||
}
|
||||
return l, true
|
||||
}
|
||||
|
||||
func (s *GormStore) SetStatus(id string, status Status) (Lead, bool) {
|
||||
var l Lead
|
||||
if err := s.db.Where("id = ?", id).First(&l).Error; err != nil {
|
||||
return Lead{}, false
|
||||
}
|
||||
l.Status = status
|
||||
if err := s.db.Save(&l).Error; err != nil {
|
||||
return Lead{}, false
|
||||
}
|
||||
return l, true
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package leads
|
||||
|
||||
import (
|
||||
"html"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
store Store
|
||||
}
|
||||
|
||||
func NewHandler(store Store) *Handler {
|
||||
return &Handler{store: store}
|
||||
}
|
||||
|
||||
// createRequest : entrée publique (formulaire vitrine). Validation stricte.
|
||||
type createRequest struct {
|
||||
Telegram string `json:"telegram" binding:"required,min=2,max=120"`
|
||||
Message string `json:"message" binding:"max=2000"`
|
||||
}
|
||||
|
||||
// Create enregistre un lead depuis le formulaire public.
|
||||
// Les entrées sont nettoyées (trim + échappement HTML) avant stockage.
|
||||
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
|
||||
}
|
||||
|
||||
lead := Lead{
|
||||
Telegram: sanitize(req.Telegram),
|
||||
}
|
||||
|
||||
if msg := sanitize(req.Message); msg != "" {
|
||||
lead.Message = &msg
|
||||
}
|
||||
|
||||
l, err := h.store.Create(lead)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, l)
|
||||
}
|
||||
|
||||
// List renvoie tous les leads (back-office, protégé).
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
items, err := h.store.List()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
type statusRequest struct {
|
||||
Status Status `json:"status" binding:"required"`
|
||||
}
|
||||
|
||||
// SetStatus met à jour le statut d'un lead (back-office, protégé).
|
||||
func (h *Handler) SetStatus(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req statusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil || !req.Status.Valid() {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "statut invalide"})
|
||||
return
|
||||
}
|
||||
l, ok := h.store.SetStatus(id, req.Status)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "lead introuvable"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, l)
|
||||
}
|
||||
|
||||
// sanitize : trim + échappement HTML pour neutraliser le XSS stocké.
|
||||
func sanitize(s string) string {
|
||||
return html.EscapeString(strings.TrimSpace(s))
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// Package leads : gestion des prospects (feature "leads" du back-office).
|
||||
package leads
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusNew Status = "new"
|
||||
StatusContacted Status = "contacted"
|
||||
StatusDemo Status = "demo"
|
||||
StatusWon Status = "won"
|
||||
StatusLost Status = "lost"
|
||||
)
|
||||
|
||||
func (s Status) Valid() bool {
|
||||
switch s {
|
||||
case StatusNew, StatusContacted, StatusDemo, StatusWon, StatusLost:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Lead : prospect. Modèle GORM.
|
||||
type Lead struct {
|
||||
ID string `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
Telegram string `gorm:"size:120;not null" json:"telegram"`
|
||||
Message *string `gorm:"size:2000" json:"message"`
|
||||
Status Status `gorm:"size:20;not null;index" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName force le nom de table.
|
||||
func (Lead) TableName() string { return "leads" }
|
||||
|
||||
// Store : persistance des leads. Impl mémoire ici, Postgres plus tard.
|
||||
type Store interface {
|
||||
Create(l Lead) (Lead, error)
|
||||
List() ([]Lead, error)
|
||||
Get(id string) (Lead, bool)
|
||||
SetStatus(id string, s Status) (Lead, bool)
|
||||
}
|
||||
|
||||
// MemStore : implémentation en mémoire (dev/tests).
|
||||
type MemStore struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]Lead
|
||||
}
|
||||
|
||||
func NewMemStore() *MemStore {
|
||||
return &MemStore{items: make(map[string]Lead)}
|
||||
}
|
||||
|
||||
func (m *MemStore) Create(l Lead) (Lead, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
l.ID = uuid.NewString()
|
||||
l.Status = StatusNew
|
||||
l.CreatedAt = time.Now().UTC()
|
||||
m.items[l.ID] = l
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (m *MemStore) List() ([]Lead, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
out := make([]Lead, 0, len(m.items))
|
||||
for _, l := range m.items {
|
||||
out = append(out, l)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *MemStore) Get(id string) (Lead, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
l, ok := m.items[id]
|
||||
return l, ok
|
||||
}
|
||||
|
||||
func (m *MemStore) SetStatus(id string, s Status) (Lead, bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
l, ok := m.items[id]
|
||||
if !ok {
|
||||
return Lead{}, false
|
||||
}
|
||||
l.Status = s
|
||||
m.items[id] = l
|
||||
return l, true
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/omnex/control-plane/api/internal/config"
|
||||
"github.com/omnex/control-plane/api/internal/demos"
|
||||
"github.com/omnex/control-plane/api/internal/httpx"
|
||||
"github.com/omnex/control-plane/api/internal/leads"
|
||||
"github.com/omnex/control-plane/api/internal/profile"
|
||||
"github.com/omnex/control-plane/api/internal/session"
|
||||
"github.com/omnex/control-plane/api/internal/sub"
|
||||
@@ -22,7 +21,6 @@ type Deps struct {
|
||||
Issuer *auth.Issuer
|
||||
Sessions session.Manager
|
||||
AuthH *auth.Handler
|
||||
LeadsH *leads.Handler
|
||||
DemosH *demos.Handler
|
||||
SubH *sub.Handler
|
||||
ProfileH *profile.Handler
|
||||
@@ -42,10 +40,9 @@ func New(d Deps) *gin.Engine {
|
||||
|
||||
api := r.Group("/api/v1")
|
||||
|
||||
// Public : login, inscription + formulaire de contact, rate-limités.
|
||||
// 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)
|
||||
api.POST("/leads", httpx.RateLimit(1, 3), d.LeadsH.Create)
|
||||
// Toute route authentifiée (session valide, n'importe quel rôle).
|
||||
authed := api.Group("")
|
||||
authed.Use(auth.RequireAuth(d.Issuer, d.Sessions))
|
||||
@@ -60,8 +57,6 @@ func New(d Deps) *gin.Engine {
|
||||
client := authed.Group("")
|
||||
client.Use(auth.RequireRole(auth.RoleClient))
|
||||
{
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user