package middleware import ( "context" "net/http" "os" "reflect" "strings" "time" "veza-backend-api/internal/response" "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) } // PresenceUpdater updates user presence (v0.301 Lot P1) type PresenceUpdater interface { UpdatePresence(ctx context.Context, userID uuid.UUID, status string) error } // TokenBlacklistChecker checks if a token is blacklisted (VEZA-SEC-006) type TokenBlacklistChecker interface { IsBlacklisted(ctx context.Context, token string) (bool, error) } // TwoFactorChecker checks if a user has 2FA enabled (SFIX-001) type TwoFactorChecker interface { GetTwoFactorStatus(ctx context.Context, userID uuid.UUID) (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 // v0.102: Supports X-API-Key for developer API keys (when apiKeyService is set) // v0.301: Optional presence update on each authenticated request type AuthMiddleware struct { sessionService SessionValidator auditService AuditRecorder permissionService PermissionChecker jwtService *services.JWTService // T0204: Use JWTService for validation userService *services.UserService // T0204: Check TokenVersion apiKeyService *services.APIKeyService // v0.102: Optional, for X-API-Key auth presenceService PresenceUpdater // v0.301: Optional, updates last_seen_at on auth tokenBlacklist TokenBlacklistChecker // VEZA-SEC-006: Optional, nil if Redis unavailable twoFactorChecker TwoFactorChecker // SFIX-001: Optional, for MFA enforcement userRateLimiter *UserRateLimiter // BE-SVC-002: Optional, per-user rate limiting applied post-auth logger *zap.Logger } // NewAuthMiddleware crée un nouveau middleware d'authentification // apiKeyService can be nil; when set, X-API-Key header is accepted as alternative to JWT // tokenBlacklist can be nil; when set (Redis available), blacklisted tokens are rejected (VEZA-SEC-006) func NewAuthMiddleware( sessionService SessionValidator, auditService AuditRecorder, permissionService PermissionChecker, jwtService *services.JWTService, userService *services.UserService, apiKeyService *services.APIKeyService, tokenBlacklist TokenBlacklistChecker, logger *zap.Logger, ) *AuthMiddleware { return &AuthMiddleware{ sessionService: sessionService, auditService: auditService, permissionService: permissionService, jwtService: jwtService, userService: userService, apiKeyService: apiKeyService, tokenBlacklist: tokenBlacklist, logger: logger, } } // SetPresenceService sets the presence service for updating last_seen_at (v0.301 Lot P1) func (am *AuthMiddleware) SetPresenceService(ps PresenceUpdater) { am.presenceService = ps } // SetTwoFactorChecker sets the 2FA checker for MFA enforcement (SFIX-001) func (am *AuthMiddleware) SetTwoFactorChecker(tfc TwoFactorChecker) { am.twoFactorChecker = tfc } // SetUserRateLimiter wires the per-user rate limiter so it runs automatically // after every successful RequireAuth / RequireAuthWithMFA call. Centralising it // here avoids sprinkling UserRateLimiter.Middleware() across every protected // route group. nil is fine — limiter simply skipped when absent. // (BE-SVC-002) func (am *AuthMiddleware) SetUserRateLimiter(url *UserRateLimiter) { am.userRateLimiter = url } // isSessionCheckRequest returns true for GET /auth/me (or path ending with /auth/me). // Used to avoid WARN logs when the frontend probes session without a token (expected case). func isSessionCheckRequest(path string) bool { return strings.HasSuffix(path, "/auth/me") || path == "/auth/me" } // extractAPIKeyFromRequest extracts API key from X-API-Key or Authorization: Bearer (for developer keys) func extractAPIKeyFromRequest(c *gin.Context) string { if k := c.GetHeader("X-API-Key"); k != "" { return k } authHeader := c.GetHeader("Authorization") if authHeader != "" { parts := strings.SplitN(authHeader, " ", 2) if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") { return parts[1] } } return "" } // 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) { // SECURITY: Try cookie first (new method), fallback to Authorization header (backward compatibility) tokenString := "" if cookie, err := c.Cookie("access_token"); err == nil && cookie != "" { tokenString = cookie } else { // Fallback to Authorization header (backward compatibility) authHeader := c.GetHeader("Authorization") if authHeader != "" { tokenParts := strings.Split(authHeader, " ") if len(tokenParts) == 2 && tokenParts[0] == "Bearer" { tokenString = tokenParts[1] } } } if tokenString == "" { // v0.102: Try X-API-Key when no JWT (developer API keys) if am.apiKeyService != nil { if apiKey := extractAPIKeyFromRequest(c); apiKey != "" && strings.HasPrefix(apiKey, "vza_") { key, err := am.apiKeyService.ValidateAPIKey(c.Request.Context(), apiKey) if err == nil { c.Set("user_id", key.UserID) c.Set("api_key", key) return key.UserID, true } am.logger.Warn("Invalid API key", zap.String("ip", c.ClientIP())) response.Unauthorized(c, "Invalid API key") c.Abort() return uuid.Nil, false } } if isSessionCheckRequest(c.Request.URL.Path) { am.logger.Debug("Missing access token (cookie or Authorization header)", zap.String("path", c.Request.URL.Path), zap.String("ip", c.ClientIP()), ) } else { am.logger.Warn("Missing access token (cookie or Authorization header)", zap.String("ip", c.ClientIP()), zap.String("user_agent", c.GetHeader("User-Agent")), ) } response.Unauthorized(c, "Access token required") c.Abort() return uuid.Nil, false } // v0.102: If Bearer token looks like API key (vza_ prefix), try API key auth if am.apiKeyService != nil && strings.HasPrefix(tokenString, "vza_") { key, err := am.apiKeyService.ValidateAPIKey(c.Request.Context(), tokenString) if err == nil { c.Set("user_id", key.UserID) c.Set("api_key", key) return key.UserID, true } } // 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()), ) response.Unauthorized(c, "Invalid token") c.Abort() return uuid.Nil, false } // VEZA-SEC-006: Check token blacklist (revoked tokens) // Note: am.tokenBlacklist may be non-nil interface holding nil pointer when Redis is unavailable if am.tokenBlacklist != nil && !reflect.ValueOf(am.tokenBlacklist).IsNil() { blacklisted, err := am.tokenBlacklist.IsBlacklisted(c.Request.Context(), tokenString) if err != nil { am.logger.Warn("Token blacklist check failed", zap.Error(err)) response.Unauthorized(c, "Invalid token") c.Abort() return uuid.Nil, false } if blacklisted { response.Unauthorized(c, "Token revoked") c.Abort() return uuid.Nil, false } } userID := claims.UserID // T0204: Check TokenVersion against DB to ensure immediate revocation user, err := am.userService.GetByID(c.Request.Context(), userID) if err != nil { am.logger.Warn("User not found during auth", zap.Error(err), zap.String("user_id", userID.String()), ) response.Unauthorized(c, "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), ) response.Unauthorized(c, "Token revoked") c.Abort() return uuid.Nil, false } session, err := am.sessionService.ValidateSession(c.Request.Context(), tokenString) if err != nil { // Check if context was cancelled/timed out if ctxErr := c.Request.Context().Err(); ctxErr != nil { am.logger.Warn("Context cancelled during session validation", zap.Error(ctxErr), zap.String("user_id", userID.String()), ) c.Abort() return uuid.Nil, false } am.logger.Warn("Invalid session", zap.Error(err), zap.String("user_id", userID.String()), zap.String("ip", c.ClientIP()), ) response.Unauthorized(c, "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()), ) response.Forbidden(c, "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) // BE-SEC-008: Automatic session refresh if session is close to expiration // Refresh session if it expires within the next 25% of its lifetime sessionLifetime := session.ExpiresAt.Sub(session.CreatedAt) timeUntilExpiry := time.Until(session.ExpiresAt) refreshThreshold := sessionLifetime / 4 // Refresh when 25% of lifetime remains if timeUntilExpiry < refreshThreshold && timeUntilExpiry > 0 { // Calculate new expiration (extend by original lifetime) newExpiresIn := sessionLifetime if newExpiresIn == 0 { newExpiresIn = 7 * 24 * time.Hour // SECURITY(SFIX-002): Default 7 days per ORIGIN Rule 4 } // Refresh the session asynchronously (non-blocking) go func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := am.sessionService.RefreshSession(ctx, tokenString, newExpiresIn); err != nil { am.logger.Warn("Failed to auto-refresh session", zap.Error(err), zap.String("user_id", userID.String()), zap.String("session_id", session.ID.String()), ) } else { am.logger.Debug("Session auto-refreshed", zap.String("user_id", userID.String()), zap.String("session_id", session.ID.String()), zap.Duration("new_expires_in", newExpiresIn), ) } }() } // 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) { userID, ok := am.authenticate(c) if !ok { return } // v0.301 Lot P1: Update presence (last_seen_at) on each authenticated request if am.presenceService != nil { go func() { _ = am.presenceService.UpdatePresence(context.Background(), userID, "online") }() } // BE-SVC-002: Per-user rate limiting runs after auth so user_id is set. // Limiter writes 429 + X-RateLimit-* headers and aborts the chain if // the user exceeds their window; c.Next() below only fires when // the limiter lets the request through. if am.userRateLimiter != nil { am.userRateLimiter.Middleware()(c) if c.IsAborted() { return } } c.Next() } } // OptionalAuth middleware d'authentification optionnelle // MIGRATION UUID: Simplifié, utilise UUID directement func (am *AuthMiddleware) OptionalAuth() gin.HandlerFunc { return func(c *gin.Context) { // SECURITY: Try cookie first (new method), fallback to Authorization header (backward compatibility) tokenString := "" if cookie, err := c.Cookie("access_token"); err == nil && cookie != "" { tokenString = cookie } else { // Fallback to Authorization header (backward compatibility) authHeader := c.GetHeader("Authorization") if authHeader != "" { tokenParts := strings.Split(authHeader, " ") if len(tokenParts) == 2 && tokenParts[0] == "Bearer" { tokenString = tokenParts[1] } } } if tokenString == "" { c.Next() return } 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(c.Request.Context(), 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 { // Check if context was cancelled/timed out if ctxErr := c.Request.Context().Err(); ctxErr != nil { c.Abort() return } 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) // BE-SEC-008: Automatic session refresh if session is close to expiration // Refresh session if it expires within the next 25% of its lifetime sessionLifetime := session.ExpiresAt.Sub(session.CreatedAt) timeUntilExpiry := time.Until(session.ExpiresAt) refreshThreshold := sessionLifetime / 4 // Refresh when 25% of lifetime remains if timeUntilExpiry < refreshThreshold && timeUntilExpiry > 0 { // Calculate new expiration (extend by original lifetime) newExpiresIn := sessionLifetime if newExpiresIn == 0 { newExpiresIn = 7 * 24 * time.Hour // SECURITY(SFIX-002): Default 7 days per ORIGIN Rule 4 } // Refresh the session asynchronously (non-blocking) go func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := am.sessionService.RefreshSession(ctx, tokenString, newExpiresIn); err != nil { am.logger.Warn("Failed to auto-refresh session (optional auth)", zap.Error(err), zap.String("user_id", userID.String()), zap.String("session_id", session.ID.String()), ) } else { am.logger.Debug("Session auto-refreshed (optional auth)", zap.String("user_id", userID.String()), zap.String("session_id", session.ID.String()), zap.Duration("new_expires_in", newExpiresIn), ) } }() } 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)) response.InternalServerError(c, "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()), ) response.Forbidden(c, "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() } } // RequireMFA middleware that enforces MFA for privileged roles (admin, moderator). // SECURITY(SFIX-001): ORIGIN_SECURITY_FRAMEWORK.md Rule 5 — MFA OBLIGATOIRE pour admin et moderator. // Must be applied AFTER RequireAuth()/RequireAdmin(). If the user has a privileged role // and has not enabled 2FA, returns 403 with error code "mfa_setup_required". func (am *AuthMiddleware) RequireMFA() gin.HandlerFunc { return func(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { response.Unauthorized(c, "Authentication required") c.Abort() return } uid, ok := userID.(uuid.UUID) if !ok { response.Unauthorized(c, "Invalid user context") c.Abort() return } // Look up the user's role user, err := am.userService.GetByID(c.Request.Context(), uid) if err != nil { am.logger.Warn("RequireMFA: user not found", zap.String("user_id", uid.String()), zap.Error(err)) response.Unauthorized(c, "User not found") c.Abort() return } // Only enforce MFA for privileged roles role := strings.ToLower(user.Role) if role != "admin" && role != "moderator" { c.Next() return } // Check 2FA status if am.twoFactorChecker == nil { am.logger.Warn("RequireMFA: TwoFactorChecker not configured, blocking privileged access", zap.String("user_id", uid.String()), zap.String("role", role), ) c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": gin.H{ "code": "mfa_setup_required", "message": "Two-factor authentication must be enabled for " + role + " accounts", }, }) return } enabled, err := am.twoFactorChecker.GetTwoFactorStatus(c.Request.Context(), uid) if err != nil { am.logger.Error("RequireMFA: failed to check 2FA status", zap.String("user_id", uid.String()), zap.Error(err), ) response.InternalServerError(c, "Failed to verify MFA status") c.Abort() return } if !enabled { am.logger.Warn("RequireMFA: privileged user without MFA denied access", zap.String("user_id", uid.String()), zap.String("role", role), zap.String("endpoint", c.Request.URL.Path), ) c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": gin.H{ "code": "mfa_setup_required", "message": "Two-factor authentication must be enabled for " + role + " accounts. Please set up 2FA via /auth/2fa/setup.", }, }) return } 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)) response.InternalServerError(c, "Internal server error") c.Abort() return } if !hasPermission { am.logger.Warn("Permission denied", zap.String("user_id", userID.String()), zap.String("permission", permission), ) response.Forbidden(c, "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 // MVP: En développement, autoriser tous les utilisateurs authentifiés func (am *AuthMiddleware) RequireContentCreatorRole() gin.HandlerFunc { return func(c *gin.Context) { userID, ok := am.authenticate(c) if !ok { return } // Role bypass only when explicitly opted-in for dev/test. env := os.Getenv("ENVIRONMENT") if env == "" { env = os.Getenv("APP_ENV") } if os.Getenv("BYPASS_CONTENT_CREATOR_ROLE") == "true" && (env == "development" || env == "dev" || env == "test") { am.logger.Debug("Bypassing role check (BYPASS_CONTENT_CREATOR_ROLE=true)", zap.String("user_id", userID.String()), zap.String("environment", env), ) c.Next() 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), ) response.Forbidden(c, "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() } } // ResourceOwnerResolver est une fonction qui récupère l'ID du propriétaire d'une ressource // depuis le contexte de la requête (paramètres de route, etc.) // Retourne l'UUID du propriétaire et une erreur si la ressource n'existe pas type ResourceOwnerResolver func(c *gin.Context) (uuid.UUID, error) // RequireOwnershipOrAdmin middleware qui vérifie que l'utilisateur authentifié est le propriétaire // de la ressource ou qu'il a le rôle admin // MOD-P0-003: Middleware générique pour vérification ownership centralisée func (am *AuthMiddleware) RequireOwnershipOrAdmin(resourceType string, resolver ResourceOwnerResolver) gin.HandlerFunc { return func(c *gin.Context) { // Authentifier l'utilisateur userID, ok := am.authenticate(c) if !ok { return } // Récupérer l'ID du propriétaire de la ressource via le resolver resourceOwnerID, err := resolver(c) if err != nil { am.logger.Warn("Failed to resolve resource owner", zap.Error(err), zap.String("resource_type", resourceType), zap.String("user_id", userID.String()), ) response.NotFound(c, "Resource not found") c.Abort() return } // Si l'utilisateur est le propriétaire, autoriser if userID == resourceOwnerID { c.Next() return } // Vérifier si l'utilisateur est admin hasRole, err := am.permissionService.HasRole(c.Request.Context(), userID, "admin") if err != nil { am.logger.Error("Failed to check admin role for ownership", zap.Error(err), zap.String("resource_type", resourceType), zap.String("user_id", userID.String()), ) response.InternalServerError(c, "Internal server error") c.Abort() return } if hasRole { am.logger.Info("Admin override for ownership check", zap.String("resource_type", resourceType), zap.String("user_id", userID.String()), zap.String("resource_owner_id", resourceOwnerID.String()), ) c.Next() return } // L'utilisateur n'est ni le propriétaire ni admin → Forbidden am.logger.Warn("Ownership check failed", zap.String("resource_type", resourceType), zap.String("user_id", userID.String()), zap.String("resource_owner_id", resourceOwnerID.String()), zap.String("ip", c.ClientIP()), ) response.Forbidden(c, "You do not have permission to access this resource") c.Abort() } } // 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 == "" { response.Unauthorized(c, "Authorization header required") c.Abort() return } tokenParts := strings.Split(authHeader, " ") if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { response.Unauthorized(c, "Invalid Authorization header format") c.Abort() return } tokenString := tokenParts[1] claims, err := am.jwtService.ValidateToken(tokenString) if err != nil { response.Unauthorized(c, "Invalid token") c.Abort() return } userID := claims.UserID // T0204: Check TokenVersion user, err := am.userService.GetByID(c.Request.Context(), userID) if err != nil { response.Unauthorized(c, "User not found") c.Abort() return } if err := am.jwtService.VerifyTokenVersion(claims, user.TokenVersion); err != nil { response.Unauthorized(c, "Token revoked") c.Abort() return } session, err := am.sessionService.ValidateSession(c.Request.Context(), tokenString) if err != nil { // Check if context was cancelled/timed out if ctxErr := c.Request.Context().Err(); ctxErr != nil { c.Abort() return } response.Unauthorized(c, "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()), ) response.InternalServerError(c, "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(), }) } }