feat: add api
docker-publish / publish (push) Failing after 14m14s

This commit is contained in:
Nuxgrid
2026-07-27 16:06:19 +02:00
parent 8397a4470f
commit 6b4274d12b
46 changed files with 3922 additions and 0 deletions
@@ -0,0 +1,57 @@
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
}
@@ -0,0 +1,77 @@
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:"company" 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
}
l, err := h.store.Create(Lead{
Telegram: sanitize(req.Telegram),
Message: sanitize(req.Message),
})
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))
}
+96
View File
@@ -0,0 +1,96 @@
// 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
}