Files
2026-06-14 17:50:35 +02:00

510 lines
14 KiB
Go

// ============================================
// 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
)
// ============================================
// VALIDATION TOKENS
// ============================================
// validateClientToken valide un token client
func validateClientToken(tokenString string) (*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) (any, 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) (*AdminClaims, error) {
tokenString = strings.TrimSpace(tokenString)
if tokenString == "" {
return nil, fmt.Errorf("token vide")
}
log.Printf("🔍 [VALIDATE-ADMIN] Validating admin token...")
token, err := jwt.ParseWithClaims(tokenString, &AdminClaims{}, func(token *jwt.Token) (any, 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
}
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)
if err != nil {
log.Printf("❌ [CLIENT-MWARE] Token invalide: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token invalide"})
c.Abort()
return
}
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
}
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()
}
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)
if err != nil {
log.Printf("❌ [ADMIN-MWARE] Token invalide: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token admin invalide"})
c.Abort()
return
}
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 — admin uniquement
if claims.Role != "admin" {
log.Printf("❌ [ADMIN-MWARE] Role invalide: %s (admin requis)", claims.Role)
c.JSON(http.StatusForbidden, gin.H{"error": "Droits insuffisants - Accès admin requis"})
c.Abort()
return
}
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()
}
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)
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 rôle — admin ou cabine uniquement
if claims.Role != "admin" && claims.Role != "cabine" {
log.Printf("❌ [CABINE-MWARE] Role non autorisé: %s", claims.Role)
c.JSON(http.StatusForbidden, gin.H{"error": "Droits insuffisants - Accès cabine requis"})
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()
}
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)
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 rôle — admin ou livreur uniquement
if claims.Role != "admin" && claims.Role != "livreur" {
log.Printf("❌ [LIVREUR-MWARE] Role non autorisé: %s", claims.Role)
c.JSON(http.StatusForbidden, gin.H{"error": "Droits insuffisants - Accès livreur requis"})
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()
}
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()
}
func RateLimitMiddleware(c *gin.Context) {
// Récupérer le client_id
clientID, ok := c.Get("client_id")
if !ok {
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
}
if count == 1 {
db.Redis.Expire(db.RedisCtx, rateLimitKey, 60*time.Second) // 1 minute
}
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
}
c.Header("X-RateLimit-Remaining", strconv.FormatInt(100-count, 10))
log.Printf("📊 [RATELIMIT] Client %d: %d/%d requêtes", clientIDInt, count, 100)
c.Next()
}
// LoginRateLimitMiddleware limite les tentatives de connexion par IP.
// Config: 10 tentatives par 15 minutes.
func LoginRateLimitMiddleware(c *gin.Context) {
ip := c.ClientIP()
rateLimitKey := "ratelimit:login:" + ip
count, err := db.Redis.Incr(db.RedisCtx, rateLimitKey).Result()
if err != nil {
log.Printf("⚠️ [LOGIN-RATELIMIT] Erreur Redis: %v", err)
c.Next()
return
}
if count == 1 {
db.Redis.Expire(db.RedisCtx, rateLimitKey, 15*time.Minute)
}
if count > 10 {
log.Printf("❌ [LOGIN-RATELIMIT] IP %s bloquée: %d tentatives/15min", ip, count)
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Trop de tentatives de connexion - Réessayez dans 15 minutes",
})
c.Abort()
return
}
c.Header("X-RateLimit-Remaining", strconv.FormatInt(10-count, 10))
c.Next()
}