// ============================================ // 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 ", }) c.Abort() return } // Vérifier le format "Bearer " 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 ", }) 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() } }