chore: add ansible backend docker frontend-prep

This commit is contained in:
2026-01-21 13:05:13 +01:00
parent 5a280b6b01
commit 943fe4de7d
14930 changed files with 2341433 additions and 0 deletions
@@ -0,0 +1,653 @@
// ============================================
// middleware/middleware_CORRIGES.go
// ============================================
// Réorganisation complète des middlewares
// Déplace ClientMiddleware, AdminMiddleware, etc depuis handlers/auth.go
package middleware
import (
"fmt"
"gestion/db"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// ============================================
// TYPES JWT CLAIMS
// ============================================
type ClientClaims struct {
ClientID int `json:"client_id"`
Username string `json:"username"`
Role string `json:"role"`
SessionID string `json:"session_id"`
jwt.RegisteredClaims
}
type AdminClaims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
SessionID string `json:"session_id"`
jwt.RegisteredClaims
}
// ============================================
// VARIABLES GLOBALES
// ============================================
var (
userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET")) // ✅ Pour clients
adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET")) // ✅ Pour admin/cabine/livreur
roleHierarchy = map[string][]string{
"admin": {"admin", "cabine", "livreur", "client"},
"cabine": {"cabine", "livreur", "client"},
"livreur": {"livreur", "client"},
}
)
// ============================================
// VALIDATION TOKENS
// ============================================
// validateClientToken valide un token client
func validateClientToken(tokenString string, database *db.Database) (*ClientClaims, error) {
tokenString = strings.TrimSpace(tokenString)
if tokenString == "" {
return nil, fmt.Errorf("token vide")
}
log.Printf("🔍 [VALIDATE-CLIENT] Validating client token...")
// Parser JWT EN PREMIER avec userJWTSecret (CLIENT)
token, err := jwt.ParseWithClaims(tokenString, &ClientClaims{}, func(token *jwt.Token) (interface{}, error) {
// Vérifier explicitement l'algorithme
if token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
return nil, fmt.Errorf("unexpected signing algorithm: %v", token.Method.Alg())
}
return userJWTSecret, nil
})
if err != nil {
log.Printf("❌ [VALIDATE-CLIENT] JWT parse error: %v", err)
return nil, fmt.Errorf("jwt parsing failed: %v", err)
}
if !token.Valid {
log.Printf("❌ [VALIDATE-CLIENT] Token not valid")
return nil, fmt.Errorf("token not valid")
}
claims, ok := token.Claims.(*ClientClaims)
if !ok {
log.Printf("❌ [VALIDATE-CLIENT] Claims type error")
return nil, fmt.Errorf("invalid claims type")
}
if claims.Role == "" {
return nil, fmt.Errorf("role manquant dans le token")
}
if claims.Issuer != "api-client" {
return nil, fmt.Errorf("issuer invalide")
}
if claims.ExpiresAt.Unix() < time.Now().Unix() {
return nil, fmt.Errorf("token expiré")
}
log.Printf("✅ [VALIDATE-CLIENT] JWT valid - Username: %s, ClientID: %d", claims.Username, claims.ClientID)
return claims, nil
}
// validateAdminToken valide un token admin
func validateAdminToken(tokenString string, database *db.Database) (*AdminClaims, error) {
tokenString = strings.TrimSpace(tokenString)
if tokenString == "" {
return nil, fmt.Errorf("token vide")
}
log.Printf("🔍 [VALIDATE-ADMIN] Validating admin token...")
// Parser JWT EN PREMIER avec adminJWTSecret (ADMIN/CABINE/LIVREUR)
token, err := jwt.ParseWithClaims(tokenString, &AdminClaims{}, func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
return nil, fmt.Errorf("unexpected signing algorithm: %v", token.Method.Alg())
}
return adminJWTSecret, nil
})
if err != nil {
log.Printf("❌ [VALIDATE-ADMIN] JWT parse error: %v", err)
return nil, fmt.Errorf("jwt parsing failed: %v", err)
}
if !token.Valid {
log.Printf("❌ [VALIDATE-ADMIN] Token not valid")
return nil, fmt.Errorf("token not valid")
}
claims, ok := token.Claims.(*AdminClaims)
if !ok {
log.Printf("❌ [VALIDATE-ADMIN] Claims type error")
return nil, fmt.Errorf("invalid claims type")
}
if claims.Role == "" {
return nil, fmt.Errorf("role manquant dans le token")
}
if claims.Issuer != "api-admin" {
return nil, fmt.Errorf("issuer invalide")
}
if claims.ExpiresAt.Unix() < time.Now().Unix() {
return nil, fmt.Errorf("token expiré")
}
log.Printf("✅ [VALIDATE-ADMIN] JWT valid - Username: %s, Role: %s, UserID: %d Issuer: %s ExpiresAt: %d",
claims.Username, claims.Role, claims.UserID, claims.Issuer, claims.ExpiresAt.Unix())
return claims, nil
}
// ============================================
// MIDDLEWARE AUTHENTIFICATION CLIENT
// ============================================
// ClientMiddleware valide le JWT d'un client
func ClientMiddleware(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
log.Printf("❌ [CLIENT-MWARE] Authorization header manquant")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token d'autorisation requis"})
c.Abort()
return
}
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database := c.MustGet("database").(*db.Database)
claims, err := validateClientToken(tokenStr, database)
if err != nil {
log.Printf("❌ [CLIENT-MWARE] Token invalide: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token invalide"})
c.Abort()
return
}
// ✅ NOUVEAU : Vérifier que le token n'a pas été révoqué
valid, err := database.IsTokenValid(tokenStr)
if err != nil {
log.Printf("❌ [CLIENT-MWARE] Erreur vérification token DB: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur serveur"})
c.Abort()
return
}
if !valid {
log.Printf("❌ [CLIENT-MWARE] Token révoqué ou expiré")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token révoqué ou expiré"})
c.Abort()
return
}
// Stocker les infos du client dans le contexte
c.Set("client_id", claims.ClientID)
c.Set("username", claims.Username)
c.Set("role", claims.Role)
c.Set("session_id", claims.SessionID)
log.Printf("✅ [CLIENT-MWARE] Client %s (ID=%d) authentifié", claims.Username, claims.ClientID)
c.Next()
}
// ============================================
// MIDDLEWARE AUTHENTIFICATION ADMIN
// ============================================
// AdminMiddleware valide le JWT d'un admin (role == "admin" SEULEMENT)
// ✅ Remplace le AdminMiddleware de handlers/auth.go
func AdminMiddleware(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
log.Printf("❌ [ADMIN-MWARE] Authorization header manquant")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token d'autorisation requis"})
c.Abort()
return
}
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database := c.MustGet("database").(*db.Database)
claims, err := validateAdminToken(tokenStr, database)
if err != nil {
log.Printf("❌ [ADMIN-MWARE] Token invalide: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token admin invalide"})
c.Abort()
return
}
// ✅ Check révocation
valid, err := database.IsTokenValid(tokenStr)
if err != nil {
log.Printf("❌ [ADMIN-MWARE] Erreur vérification token DB: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur serveur"})
c.Abort()
return
}
if !valid {
log.Printf("❌ [ADMIN-MWARE] Token révoqué ou expiré")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token révoqué ou expiré"})
c.Abort()
return
}
// Vérifier rôle
validRoles := []string{"admin", "cabine", "livreur"}
isValid := false
for _, role := range validRoles {
if claims.Role == role {
isValid = true
break
}
}
if !isValid {
log.Printf("❌ [ADMIN-MWARE] Role invalide: %s", claims.Role)
c.JSON(http.StatusForbidden, gin.H{"error": "Droits insuffisants - Accès admin requis"})
c.Abort()
return
}
// Stocker les infos de l'admin dans le contexte
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("role", claims.Role)
c.Set("session_id", claims.SessionID)
log.Printf("✅ [ADMIN-MWARE] Admin %s (Role=%s, ID=%d) authentifié",
claims.Username, claims.Role, claims.UserID)
c.Next()
}
// ============================================
// MIDDLEWARE AUTHENTIFICATION CABINE
// ============================================
// CabineMiddleware valide que l'utilisateur a accès à la cabine
// ✅ Remplace le CabineMiddleware de handlers/auth.go
func CabineMiddleware(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
log.Printf("❌ [CABINE-MWARE] Authorization header manquant")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token d'autorisation requis"})
c.Abort()
return
}
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database := c.MustGet("database").(*db.Database)
claims, err := validateAdminToken(tokenStr, database)
if err != nil {
log.Printf("❌ [CABINE-MWARE] Token invalide: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token invalide"})
c.Abort()
return
}
// ✅ Check révocation
valid, err := database.IsTokenValid(tokenStr)
if err != nil {
log.Printf("❌ [CABINE-MWARE] Erreur vérification token DB: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur serveur"})
c.Abort()
return
}
if !valid {
log.Printf("❌ [CABINE-MWARE] Token révoqué ou expiré")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token révoqué ou expiré"})
c.Abort()
return
}
// Vérifier hiérarchie des rôles
allowedRoles := roleHierarchy[claims.Role]
authorized := false
for _, r := range allowedRoles {
if r == "cabine" {
authorized = true
break
}
}
if !authorized {
log.Printf("❌ [CABINE-MWARE] Role non autorisé: %s", claims.Role)
c.JSON(http.StatusForbidden, gin.H{
"error": "Droits insuffisants - Accès cabine requis",
"your_role": claims.Role,
"allowed_roles": "admin, cabine",
})
c.Abort()
return
}
// Stocker les infos
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("role", claims.Role)
c.Set("session_id", claims.SessionID)
log.Printf("✅ [CABINE-MWARE] Cabine %s (%s) authentifiée", claims.Username, claims.Role)
c.Next()
}
// ============================================
// MIDDLEWARE AUTHENTIFICATION LIVREUR
// ============================================
// LivreurMiddleware valide que l'utilisateur est livreur
// ✅ Remplace le LivreurMiddleware de handlers/auth.go
func LivreurMiddleware(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
log.Printf("❌ [LIVREUR-MWARE] Authorization header manquant")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token d'autorisation requis"})
c.Abort()
return
}
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database := c.MustGet("database").(*db.Database)
claims, err := validateAdminToken(tokenStr, database)
if err != nil {
log.Printf("❌ [LIVREUR-MWARE] Token invalide: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token invalide"})
c.Abort()
return
}
// ✅ Check révocation
valid, err := database.IsTokenValid(tokenStr)
if err != nil {
log.Printf("❌ [LIVREUR-MWARE] Erreur vérification token DB: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur serveur"})
c.Abort()
return
}
if !valid {
log.Printf("❌ [LIVREUR-MWARE] Token révoqué ou expiré")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token révoqué ou expiré"})
c.Abort()
return
}
// Vérifier hiérarchie des rôles
allowedRoles := roleHierarchy[claims.Role]
authorized := false
for _, r := range allowedRoles {
if r == "livreur" {
authorized = true
break
}
}
if !authorized {
log.Printf("❌ [LIVREUR-MWARE] Role non autorisé: %s", claims.Role)
c.JSON(http.StatusForbidden, gin.H{
"error": "Droits insuffisants - Accès livreur requis",
"your_role": claims.Role,
"allowed_roles": "admin, livreur",
})
c.Abort()
return
}
// Stocker les infos
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("role", claims.Role)
c.Set("session_id", claims.SessionID)
log.Printf("✅ [LIVREUR-MWARE] Livreur %s (%s) authentifié", claims.Username, claims.Role)
c.Next()
}
// ============================================
// SESSION MIDDLEWARE CLIENT (Existant)
// ============================================
// ClientSessionMiddleware valide la session Redis du client
func ClientSessionMiddleware(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
// Récupérer le username du JWT (DÉJÀ VALIDÉ)
username, exists := c.Get("username")
if !exists {
log.Printf("❌ [SESSION-MWARE] Username manquant du JWT")
c.JSON(http.StatusUnauthorized, gin.H{
"error": "JWT invalide - username manquant",
})
c.Abort()
return
}
usernameStr := username.(string)
// Récupérer le client_id du JWT
clientID, ok := c.Get("client_id")
if !ok {
log.Printf("❌ [SESSION-MWARE] client_id manquant du JWT")
c.JSON(http.StatusUnauthorized, gin.H{
"error": "JWT invalide - client_id manquant",
})
c.Abort()
return
}
clientIDInt, ok := clientID.(int)
if !ok {
log.Printf("❌ [SESSION-MWARE] client_id malformé: %v", clientID)
c.JSON(http.StatusUnauthorized, gin.H{
"error": "JWT invalide - client_id malformé",
})
c.Abort()
return
}
// Vérifier la session Redis
session, err := database.GetClientSession(clientIDInt)
if err != nil {
log.Printf("❌ [SESSION-MWARE] Pas de session Redis pour client %d: %v", clientIDInt, err)
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Session expirée - Veuillez vous reconnecter",
"action": "Please login again",
})
c.Abort()
return
}
// Vérifier que les données correspondent
if session.Username != usernameStr {
log.Printf("❌ [SESSION-MWARE] MISMATCH! JWT=%s, session=%s", usernameStr, session.Username)
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Session invalide - Mismatch détecté",
})
c.Abort()
return
}
if session.ClientID != clientIDInt {
log.Printf("❌ [SESSION-MWARE] MISMATCH! JWT=%d, session=%d", clientIDInt, session.ClientID)
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Session invalide - client_id mismatch",
})
c.Abort()
return
}
// Prolonger la session
if err := database.RefreshSessionTimeout(clientIDInt); err != nil {
log.Printf("⚠️ [SESSION-MWARE] Erreur refresh: %v", err)
}
// Charger les infos dans le contexte
c.Set("session_id", session.SessionID)
c.Set("session", session)
c.Set("last_activity", session.LastActivity)
log.Printf("✅ [SESSION-MWARE] Session valide pour %s (client_id=%d)", usernameStr, clientIDInt)
c.Next()
}
// ============================================
// RATE LIMITING MIDDLEWARE
// ============================================
// RateLimitMiddleware limite le nombre de requêtes par client
// Config: 100 requêtes par minute par client
func RateLimitMiddleware(c *gin.Context) {
// Récupérer le client_id
clientID, ok := c.Get("client_id")
if !ok {
// Pas de client_id (requête publique), pas de rate limit
c.Next()
return
}
clientIDInt := clientID.(int)
rateLimitKey := "ratelimit:" + strconv.Itoa(clientIDInt)
// Incrémenter le compteur
count, err := db.Redis.Incr(db.RedisCtx, rateLimitKey).Result()
if err != nil {
log.Printf("⚠️ [RATELIMIT] Erreur: %v", err)
c.Next()
return
}
// Initialiser le TTL à la première requête
if count == 1 {
db.Redis.Expire(db.RedisCtx, rateLimitKey, 60*time.Second) // 1 minute
}
// Vérifier si dépassement (100 requêtes/min)
if count > 100 {
log.Printf("❌ [RATELIMIT] Client %d dépassé le limite: %d requêtes/min", clientIDInt, count)
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Trop de requêtes - Réessayez dans une minute",
})
c.Abort()
return
}
// Ajouter le header du remaining
c.Header("X-RateLimit-Remaining", strconv.FormatInt(100-count, 10))
log.Printf("📊 [RATELIMIT] Client %d: %d/%d requêtes", clientIDInt, count, 100)
c.Next()
}
// ============================================
// HELPER MIDDLEWARE
// ============================================
// VerifyAuthHeader vérifie que le header Authorization est valide
func VerifyAuthHeader(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
log.Printf("❌ [AUTH-HEADER] Authorization header manquant")
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Authorization header manquant",
"hint": "Utilisez: Authorization: Bearer <token>",
})
c.Abort()
return
}
// Vérifier le format "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
log.Printf("❌ [AUTH-HEADER] Format invalide: %s", authHeader)
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Format Authorization invalide",
"hint": "Utilisez: Authorization: Bearer <token>",
})
c.Abort()
return
}
log.Printf("✅ [AUTH-HEADER] Format valide")
c.Next()
}
// SessionErrorRecovery récupère les erreurs de session
func SessionErrorRecovery(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
log.Printf("❌ [SESSION-ERROR] Erreur système: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur serveur - Session compromise",
})
}
}()
c.Next()
if len(c.Errors) > 0 {
log.Printf("⚠️ [SESSION] Erreur handler: %v", c.Errors)
}
}
// LogSessionMiddleware log toutes les infos de session
func LogSessionMiddleware(c *gin.Context) {
username, _ := c.Get("username")
clientID, _ := c.Get("client_id")
sessionID, _ := c.Get("session_id")
log.Printf("📊 [SESSION-LOG] %s %s | user=%v | client_id=%v | session=%v",
c.Request.Method, c.Request.URL.Path, username, clientID, sessionID)
c.Next()
log.Printf("📊 [SESSION-LOG] Response: %d", c.Writer.Status())
}
// LoadClientContext charge les infos du client en contexte
func LoadClientContext(c *gin.Context, database *db.Database) (*db.SessionData, error) {
clientID, ok := c.Get("client_id")
if !ok {
return nil, fmt.Errorf("client_id manquant du contexte")
}
clientIDInt := clientID.(int)
// Récupérer la session
session, err := database.GetClientSession(clientIDInt)
if err != nil {
return nil, err
}
return session, nil
}
// ============================================
// DATABASE MIDDLEWARE
// ============================================
// DatabaseMiddleware injecte la base de données dans le contexte
func DatabaseMiddleware(db *db.Database) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("database", db)
c.Next()
}
}