package middleware import ( "context" "fmt" "net/http" "strings" "time" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "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 logger *zap.Logger jwtSecret string } // 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, logger *zap.Logger, jwtSecret string, ) *AuthMiddleware { return &AuthMiddleware{ sessionService: sessionService, auditService: auditService, permissionService: permissionService, logger: logger, jwtSecret: jwtSecret, } } // 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] userID, err := am.validateJWTToken(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 } 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] userID, err := am.validateJWTToken(tokenString) if 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() } } // validateJWTToken valide un token JWT et retourne l'ID utilisateur (UUID) // MIGRATION UUID: Retourne maintenant uuid.UUID au lieu de string func (am *AuthMiddleware) validateJWTToken(tokenString string) (uuid.UUID, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } return []byte(am.jwtSecret), nil }) if err != nil { return uuid.Nil, err } if !token.Valid { return uuid.Nil, jwt.ErrTokenMalformed } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return uuid.Nil, jwt.ErrTokenMalformed } // Support 'sub' (standard) qui devrait contenir l'UUID sous forme de string if sub, ok := claims["sub"]; ok { switch v := sub.(type) { case string: uid, err := uuid.Parse(v) if err != nil { return uuid.Nil, fmt.Errorf("invalid UUID in sub claim: %w", err) } return uid, nil default: return uuid.Nil, fmt.Errorf("sub claim must be UUID string, got: %T", v) } } // Fallback sur user_id custom claim (legacy) if userIDStr, ok := claims["user_id"].(string); ok { uid, err := uuid.Parse(userIDStr) if err != nil { return uuid.Nil, fmt.Errorf("invalid UUID in user_id claim: %w", err) } return uid, nil } return uuid.Nil, jwt.ErrTokenMalformed } // 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] userID, err := am.validateJWTToken(tokenString) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) 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(), }) } }