Backend changes (Action 5.1.1.1): - Set access_token cookie in Login, Register, and Refresh handlers - Cookie uses same configuration as refresh_token (httpOnly, Secure, SameSite) - Expiry matches AccessTokenTTL (5 minutes) - Update logout handler to clear access_token cookie Backend middleware (Action 5.1.1.1): - Update auth middleware to read access token from cookie first - Fallback to Authorization header for backward compatibility - Update OptionalAuth with same cookie-first logic Frontend changes (Actions 5.1.1.2 & 5.1.1.3): - Remove localStorage token storage from TokenStorage service - TokenStorage now returns null for getAccessToken/getRefreshToken (httpOnly cookies not accessible) - Remove Authorization header logic from API client - Remove token expiration checks (can't check httpOnly cookies from JS) - Update AuthContext to remove localStorage usage - Update tokenRefresh to work without reading tokens from JS - Simplify refresh logic: periodic refresh every 4 minutes (no expiration checks) Security improvements: - Access tokens no longer exposed to XSS attacks (httpOnly cookies) - Tokens automatically sent with requests via withCredentials: true - Backend reads tokens from cookies, not Authorization headers - All users will need to re-login after deployment (breaking change) Breaking change: All users must re-login after deployment
628 lines
19 KiB
Go
628 lines
19 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"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)
|
|
}
|
|
|
|
// 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) {
|
|
// 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 == "" {
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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()),
|
|
)
|
|
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 = 30 * 24 * time.Hour // Default to 30 days
|
|
}
|
|
|
|
// 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) {
|
|
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) {
|
|
// 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(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 = 30 * 24 * time.Hour // Default to 30 days
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// MVP: En développement, autoriser tous les utilisateurs authentifiés
|
|
env := os.Getenv("ENVIRONMENT")
|
|
if env == "" {
|
|
env = os.Getenv("APP_ENV")
|
|
}
|
|
if env == "development" || env == "dev" {
|
|
am.logger.Debug("MVP: Bypassing role check in development",
|
|
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(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(),
|
|
})
|
|
}
|
|
}
|