@@ -0,0 +1,126 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/omnex/control-plane/api/internal/auth"
|
||||
)
|
||||
|
||||
// Handler gère les requêtes HTTP pour les codes de souscription.
|
||||
type Handler struct {
|
||||
store Store
|
||||
}
|
||||
|
||||
// NewHandler crée un nouveau Handler.
|
||||
func NewHandler(store Store) *Handler {
|
||||
return &Handler{store: store}
|
||||
}
|
||||
|
||||
// createCodeRequest représente la requête pour créer un code.
|
||||
type createCodeRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=64,alphanum"`
|
||||
}
|
||||
|
||||
type addCodeRequest struct {
|
||||
Code string `json:"code_verif" binding:"required,len=19"`
|
||||
}
|
||||
|
||||
func GenerateCode() (string, error) {
|
||||
const length = 16
|
||||
const groupSize = 4
|
||||
|
||||
b := make([]byte, length)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for i := 0; i < length; i++ {
|
||||
if i > 0 && i%groupSize == 0 {
|
||||
sb.WriteByte('-')
|
||||
}
|
||||
sb.WriteByte(codeCharset[int(b[i])%len(codeCharset)])
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
const codeCharset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
|
||||
// ListCodes retourne la liste de tous les codes de souscription.
|
||||
// Réservé aux administrateurs.
|
||||
func (h *Handler) ListCodes(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.StatusUnauthorized, gin.H{"error": "role non correcte"})
|
||||
return
|
||||
}
|
||||
codes, err := h.store.ListCodes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"items": codes})
|
||||
}
|
||||
|
||||
// CreateCodeForBuy crée un nouveau code de souscription pour un utilisateur.
|
||||
// Réservé aux administrateurs.
|
||||
func (h *Handler) CreateCodeForBuy(c *gin.Context) {
|
||||
// Vérification de l'authentification et du rôle sera faite par le middleware
|
||||
p := auth.PrincipalFrom(c)
|
||||
if p == nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
|
||||
return
|
||||
}
|
||||
if p.Role != "admin" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "role non correcte"})
|
||||
return
|
||||
}
|
||||
var req createCodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
code, err := GenerateCode()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
||||
return
|
||||
}
|
||||
|
||||
codeSub, err := h.store.CreateCodeForSub(req.Username, code)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"code": code, "code_id": codeSub.ID, "username": codeSub.Username})
|
||||
}
|
||||
|
||||
func (h *Handler) AddCode(c *gin.Context) {
|
||||
p := auth.PrincipalFrom(c)
|
||||
if p == nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
var req addCodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.store.AddCodeForSub(p.Username, req.Code)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "code invalide"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": "vous passez en abonnement premium"})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/omnex/control-plane/api/internal/auth"
|
||||
)
|
||||
|
||||
// MemCodeStore : implémentation en mémoire de CodeStore (dev/tests).
|
||||
type MemCodeStore struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]CodeBuySub
|
||||
byCode map[string]CodeBuySub
|
||||
}
|
||||
|
||||
func NewMemCodeStore() *MemCodeStore {
|
||||
return &MemCodeStore{
|
||||
items: make(map[string]CodeBuySub),
|
||||
byCode: make(map[string]CodeBuySub),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MemCodeStore) CreateCodeForSub(username, code string) (CodeBuySub, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
c := CodeBuySub{
|
||||
ID: uuid.NewString(),
|
||||
Username: username,
|
||||
CodeBuy: code,
|
||||
}
|
||||
m.items[c.ID] = c
|
||||
m.byCode[code] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (m *MemCodeStore) GetCodeForSub(username, code string) (auth.User, CodeBuySub, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
c, ok := m.byCode[code]
|
||||
if !ok {
|
||||
return auth.User{}, CodeBuySub{}, nil
|
||||
}
|
||||
// In a real implementation, we'd fetch the user too
|
||||
return auth.User{}, c, nil
|
||||
}
|
||||
|
||||
func (m *MemCodeStore) ListCodes() ([]CodeBuySub, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
out := make([]CodeBuySub, 0, len(m.items))
|
||||
for _, c := range m.items {
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CodeBuySub représente un code de souscription associé à un utilisateur.
|
||||
type CodeBuySub struct {
|
||||
ID string `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
Username string `gorm:"size:64;not null" json:"username"`
|
||||
CodeBuy string `gorm:"uniqueIndex;type:varchar(50);not null" json:"code_verif"`
|
||||
CreatedAt time.Time `gorm:"index;autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName définit explicitement le nom de la table.
|
||||
func (CodeBuySub) TableName() string { return "code_buy_subs" }
|
||||
@@ -0,0 +1,176 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/omnex/control-plane/api/internal/auth"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Store définit les opérations de persistance pour les codes de souscription.
|
||||
type Store interface {
|
||||
// CreateCodeForSub crée un nouveau code pour un utilisateur.
|
||||
CreateCodeForSub(username, code string) (CodeBuySub, error)
|
||||
// GetCodeForSub récupère un code pour un utilisateur.
|
||||
GetCodeForSub(username, code string) (CodeBuySub, error)
|
||||
// ListCodes liste tous les codes.
|
||||
ListCodes() ([]CodeBuySub, error)
|
||||
AddCodeForSub(username, code string) (auth.User, error)
|
||||
}
|
||||
|
||||
// GormStore implémente Store avec PostgreSQL via GORM.
|
||||
type GormStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormStore crée un nouveau GormStore.
|
||||
func NewGormStore(db *gorm.DB) *GormStore {
|
||||
return &GormStore{db: db}
|
||||
}
|
||||
|
||||
// CreateCodeForSub crée un nouveau code pour un utilisateur.
|
||||
func (s *GormStore) CreateCodeForSub(username, code string) (CodeBuySub, error) {
|
||||
c := CodeBuySub{
|
||||
ID: uuid.NewString(),
|
||||
Username: auth.NormalizeUsername(username),
|
||||
CodeBuy: code,
|
||||
}
|
||||
if err := s.db.Create(&c).Error; err != nil {
|
||||
return CodeBuySub{}, err
|
||||
}
|
||||
|
||||
// S'assurer que CreatedAt est bien rempli
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = time.Now().UTC()
|
||||
if err := s.db.Save(&c).Error; err != nil {
|
||||
return CodeBuySub{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetCodeForSub récupère un code pour un utilisateur.
|
||||
func (s *GormStore) GetCodeForSub(username, code string) (CodeBuySub, error) {
|
||||
var c CodeBuySub
|
||||
if err := s.db.Where("username = ? AND code_buy = ?", username, code).First(&c).Error; err != nil {
|
||||
return CodeBuySub{}, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *GormStore) AddCodeForSub(username, code string) (auth.User, error) {
|
||||
var c CodeBuySub
|
||||
err := s.db.Where("username = ? AND code_buy = ?", username, code).First(&c).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return auth.User{}, errors.New("code invalide")
|
||||
}
|
||||
return auth.User{}, err
|
||||
}
|
||||
|
||||
var u auth.User
|
||||
if err := s.db.Where("username = ?", auth.NormalizeUsername(username)).First(&u).Error; err != nil {
|
||||
return auth.User{}, err
|
||||
}
|
||||
|
||||
u.TypeAbo = "premium"
|
||||
u.ExpiredAt = time.Now().UTC().AddDate(0, 0, 30)
|
||||
if err := s.db.Model(&u).Updates(map[string]interface{}{
|
||||
"type_abo": u.TypeAbo,
|
||||
"expired_at": u.ExpiredAt,
|
||||
}).Error; err != nil {
|
||||
return auth.User{}, err
|
||||
}
|
||||
|
||||
if err := s.db.Delete(&c).Error; err != nil {
|
||||
return auth.User{}, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// ListCodes liste tous les codes, triés par date de création.
|
||||
func (s *GormStore) ListCodes() ([]CodeBuySub, error) {
|
||||
var codes []CodeBuySub
|
||||
// Essayer avec tri par date, sinon sans tri pour compatibilité
|
||||
if err := s.db.Order("created_at desc").Find(&codes).Error; err != nil {
|
||||
// Si colonne manquante, on liste sans tri
|
||||
if orderErr := s.db.Find(&codes).Error; orderErr != nil {
|
||||
return nil, orderErr
|
||||
}
|
||||
}
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
// MemStore implémente Store en mémoire (pour dev/tests).
|
||||
type MemStore struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]CodeBuySub
|
||||
byCode map[string]CodeBuySub
|
||||
}
|
||||
|
||||
// NewMemStore crée un nouveau MemStore.
|
||||
func NewMemStore() *MemStore {
|
||||
return &MemStore{
|
||||
items: make(map[string]CodeBuySub),
|
||||
byCode: make(map[string]CodeBuySub),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCodeForSub crée un nouveau code pour un utilisateur.
|
||||
func (m *MemStore) CreateCodeForSub(username, code string) (CodeBuySub, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
c := CodeBuySub{
|
||||
ID: uuid.NewString(),
|
||||
Username: username,
|
||||
CodeBuy: code,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
m.items[c.ID] = c
|
||||
m.byCode[code] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetCodeForSub récupère un code pour un utilisateur.
|
||||
func (m *MemStore) GetCodeForSub(username, code string) (CodeBuySub, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
c, ok := m.byCode[code]
|
||||
if !ok {
|
||||
return CodeBuySub{}, nil
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ListCodes liste tous les codes.
|
||||
func (m *MemStore) ListCodes() ([]CodeBuySub, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
out := make([]CodeBuySub, 0, len(m.items))
|
||||
for _, c := range m.items {
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *MemStore) AddCodeForSub(username, code string) (auth.User, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
c, ok := m.byCode[code]
|
||||
if !ok || c.Username != username {
|
||||
return auth.User{}, errors.New("code invalide")
|
||||
}
|
||||
delete(m.byCode, code)
|
||||
delete(m.items, c.ID)
|
||||
|
||||
// Le MemStore ne connaît pas les utilisateurs (pas de référence à memUsers ici) :
|
||||
// il ne peut donc pas mettre à jour TypeAbo lui-même. Cf. point ci-dessous.
|
||||
return auth.User{Username: username, TypeAbo: "premium"}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user