package middleware import ( "context" "net/http" "strings" "time" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // ÉTAPE 3.4: Interfaces pour permettre l'injection de dépendances et les tests avec mocks // SessionValidator définit l'interface pour valider les sessions type SessionValidator interface { ValidateSession(ctx context.Context, token string) (*services.Session, error) RefreshSession(ctx context.Context, token string, newExpiresIn time.Duration) error } // AuditRecorder définit l'interface pour enregistrer les actions d'audit type AuditRecorder interface { LogAction(ctx context.Context, req *services.AuditLogCreateRequest) error } // PermissionChecker définit l'interface pour vérifier les permissions type PermissionChecker interface { HasRole(ctx context.Context, userID uuid.UUID, roleName string) (bool, error) HasPermission(ctx context.Context, userID uuid.UUID, permissionName string) (bool, error) } // AuthMiddleware middleware d'authentification avec validation de session // ÉTAPE 3.4: Utilise des interfaces pour permettre l'injection de dépendances et les tests type AuthMiddleware struct { sessionService SessionValidator auditService AuditRecorder permissionService PermissionChecker jwtService *services.JWTService // T0204: Use JWTService for validation userService *services.UserService // T0204: Check TokenVersion logger *zap.Logger } // NewAuthMiddleware crée un nouveau middleware d'authentification // ÉTAPE 3.4: Accepte des interfaces au lieu de types concrets pour permettre les tests avec mocks func NewAuthMiddleware( sessionService SessionValidator, auditService AuditRecorder, permissionService PermissionChecker, jwtService *services.JWTService, userService *services.UserService, logger *zap.Logger, ) *AuthMiddleware { return &AuthMiddleware{ sessionService: sessionService, auditService: auditService, permissionService: permissionService, jwtService: jwtService, userService: userService, logger: logger, } } // authenticate performs the core authentication logic // Returns userID and true if successful, otherwise handles error response and returns false func (am *AuthMiddleware) authenticate(c *gin.Context) (uuid.UUID, bool) { authHeader := c.GetHeader("Authorization") if authHeader == "" { am.logger.Warn("Missing Authorization header", zap.String("ip", c.ClientIP()), zap.String("user_agent", c.GetHeader("User-Agent")), ) c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) c.Abort() return uuid.Nil, false } tokenParts := strings.Split(authHeader, " ") if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { am.logger.Warn("Invalid Authorization header format", zap.String("ip", c.ClientIP()), zap.String("header", authHeader), ) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"}) c.Abort() return uuid.Nil, false } tokenString := tokenParts[1] // T0204: Validate token using JWTService (checks sig, exp, iss, aud, alg) claims, err := am.jwtService.ValidateToken(tokenString) if err != nil { am.logger.Warn("Invalid JWT token", zap.Error(err), zap.String("ip", c.ClientIP()), ) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) c.Abort() return uuid.Nil, false } userID := claims.UserID // T0204: Check TokenVersion against DB to ensure immediate revocation user, err := am.userService.GetByID(userID) if err != nil { am.logger.Warn("User not found during auth", zap.Error(err), zap.String("user_id", userID.String()), ) c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"}) c.Abort() return uuid.Nil, false } if err := am.jwtService.VerifyTokenVersion(claims, user.TokenVersion); err != nil { am.logger.Warn("Token version mismatch (revoked)", zap.Error(err), zap.String("user_id", userID.String()), zap.Int("token_version", claims.TokenVersion), zap.Int("user_version", user.TokenVersion), ) c.JSON(http.StatusUnauthorized, gin.H{"error": "Token revoked"}) c.Abort() return uuid.Nil, false } session, err := am.sessionService.ValidateSession(c.Request.Context(), tokenString) if err != nil { am.logger.Warn("Invalid session", zap.Error(err), zap.String("user_id", userID.String()), zap.String("ip", c.ClientIP()), ) c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired or invalid"}) c.Abort() return uuid.Nil, false } if session.UserID != userID { am.logger.Warn("Session user mismatch", zap.String("session_user_id", session.UserID.String()), zap.String("token_user_id", userID.String()), ) c.JSON(http.StatusForbidden, gin.H{"error": "Session user mismatch"}) c.Abort() return uuid.Nil, false } c.Set("user_id", userID) c.Set("session_id", session.ID) c.Set("session_created_at", session.CreatedAt) c.Set("session_expires_at", session.ExpiresAt) // Log audit access err = am.auditService.LogAction(c.Request.Context(), &services.AuditLogCreateRequest{ UserID: &userID, Action: "api_access", Resource: "endpoint", IPAddress: c.ClientIP(), UserAgent: c.GetHeader("User-Agent"), Metadata: map[string]interface{}{ "endpoint": c.Request.URL.Path, "method": c.Request.Method, "session_id": session.ID.String(), }, }) if err != nil { am.logger.Error("Failed to log API access", zap.Error(err), zap.String("user_id", userID.String()), ) } return userID, true } // RequireAuth middleware qui exige une authentification func (am *AuthMiddleware) RequireAuth() gin.HandlerFunc { return func(c *gin.Context) { if _, ok := am.authenticate(c); ok { c.Next() } } } // OptionalAuth middleware d'authentification optionnelle // MIGRATION UUID: Simplifié, utilise UUID directement func (am *AuthMiddleware) OptionalAuth() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.Next() return } tokenParts := strings.Split(authHeader, " ") if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { c.Next() return } tokenString := tokenParts[1] claims, err := am.jwtService.ValidateToken(tokenString) if err != nil { c.Next() return } userID := claims.UserID // T0204: Check TokenVersion (optional auth should also respect revocation) user, err := am.userService.GetByID(userID) if err != nil { c.Next() return } if err := am.jwtService.VerifyTokenVersion(claims, user.TokenVersion); err != nil { c.Next() return } session, err := am.sessionService.ValidateSession(c.Request.Context(), tokenString) if err != nil { c.Next() return } // Ajouter UUID directement au contexte c.Set("user_id", userID) c.Set("session_id", session.ID) c.Set("session_created_at", session.CreatedAt) c.Set("session_expires_at", session.ExpiresAt) c.Next() } } // RequireAdmin middleware qui exige des droits administrateur // GO-001, GO-005, GO-006: Implémentation RBAC réelle avec PermissionService // MIGRATION UUID: userID est toujours uuid.UUID, plus de conversion // Note: RequireAdmin() inclut la vérification d'authentification, pas besoin d'appeler RequireAuth() séparément func (am *AuthMiddleware) RequireAdmin() gin.HandlerFunc { return func(c *gin.Context) { userID, ok := am.authenticate(c) if !ok { return } // Vérification RBAC réelle hasRole, err := am.permissionService.HasRole(c.Request.Context(), userID, "admin") if err != nil { am.logger.Error("Failed to check admin role", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) c.Abort() return } if !hasRole { am.logger.Warn("Admin access denied", zap.String("user_id", userID.String()), zap.String("ip", c.ClientIP()), ) c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"}) c.Abort() return } am.logger.Info("Admin access granted", zap.String("user_id", userID.String()), zap.String("ip", c.ClientIP()), zap.String("endpoint", c.Request.URL.Path), ) c.Next() } } // RequirePermission middleware qui exige une permission spécifique // GO-001, GO-005: Implémentation RBAC réelle avec PermissionService // MIGRATION UUID: userID est toujours uuid.UUID func (am *AuthMiddleware) RequirePermission(permission string) gin.HandlerFunc { return func(c *gin.Context) { userID, ok := am.authenticate(c) if !ok { return } // Vérification RBAC réelle hasPermission, err := am.permissionService.HasPermission(c.Request.Context(), userID, permission) if err != nil { am.logger.Error("Failed to check permission", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) c.Abort() return } if !hasPermission { am.logger.Warn("Permission denied", zap.String("user_id", userID.String()), zap.String("permission", permission), ) c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"}) c.Abort() return } am.logger.Info("Permission check passed", zap.String("user_id", userID.String()), zap.String("permission", permission), zap.String("ip", c.ClientIP()), zap.String("endpoint", c.Request.URL.Path), ) c.Next() } } // RequireContentCreatorRole middleware qui exige un rôle de créateur de contenu // GO-012: Vérifie que l'utilisateur a un des rôles: creator, premium, admin // Selon ORIGIN_SECURITY_FRAMEWORK, seuls ces rôles peuvent créer du contenu func (am *AuthMiddleware) RequireContentCreatorRole() gin.HandlerFunc { return func(c *gin.Context) { userID, ok := am.authenticate(c) if !ok { return } // Vérifier si l'utilisateur a un des rôles autorisés: creator, premium, admin allowedRoles := []string{"creator", "premium", "admin", "artist", "producer", "label"} hasAllowedRole := false var lastErr error for _, role := range allowedRoles { hasRole, err := am.permissionService.HasRole(c.Request.Context(), userID, role) if err != nil { lastErr = err continue } if hasRole { hasAllowedRole = true break } } if !hasAllowedRole { am.logger.Warn("Content creation denied - insufficient role", zap.String("user_id", userID.String()), zap.String("ip", c.ClientIP()), zap.String("endpoint", c.Request.URL.Path), ) c.JSON(http.StatusForbidden, gin.H{ "error": "Insufficient permissions. Content creation requires creator, premium, or admin role.", }) c.Abort() return } if lastErr != nil { am.logger.Error("Error checking roles (but user has allowed role)", zap.Error(lastErr)) } am.logger.Info("Content creation access granted", zap.String("user_id", userID.String()), zap.String("ip", c.ClientIP()), zap.String("endpoint", c.Request.URL.Path), ) c.Next() } } // RefreshToken middleware pour rafraîchir les tokens // MIGRATION UUID: Simplifié pour UUID func (am *AuthMiddleware) RefreshToken() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) c.Abort() return } tokenParts := strings.Split(authHeader, " ") if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"}) c.Abort() return } tokenString := tokenParts[1] claims, err := am.jwtService.ValidateToken(tokenString) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) c.Abort() return } userID := claims.UserID // T0204: Check TokenVersion user, err := am.userService.GetByID(userID) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"}) c.Abort() return } if err := am.jwtService.VerifyTokenVersion(claims, user.TokenVersion); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Token revoked"}) c.Abort() return } session, err := am.sessionService.ValidateSession(c.Request.Context(), tokenString) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired or invalid"}) c.Abort() return } newExpiresIn := 24 * time.Hour err = am.sessionService.RefreshSession(c.Request.Context(), tokenString, newExpiresIn) if err != nil { am.logger.Error("Failed to refresh session", zap.Error(err), zap.String("user_id", userID.String()), ) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh session"}) c.Abort() return } // Log le rafraîchissement am.logger.Info("Token refreshed", zap.String("user_id", userID.String()), zap.String("session_id", session.ID.String()), ) c.JSON(http.StatusOK, gin.H{ "message": "Token refreshed successfully", "expires_in": newExpiresIn.Seconds(), }) } }