package handlers import ( "context" "net/http" "strings" "time" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // SessionServiceInterfaceForSession extends SessionServiceInterface with additional methods needed by session handler type SessionServiceInterfaceForSession interface { RevokeAllUserSessions(ctx context.Context, userID uuid.UUID) (int64, error) RevokeOtherUserSessions(ctx context.Context, userID uuid.UUID, exceptTokenHash string) (int64, error) RevokeSession(ctx context.Context, token string) error GetUserSessions(ctx context.Context, userID uuid.UUID) ([]*services.Session, error) DeleteSession(ctx context.Context, tokenHash string) error RefreshSession(ctx context.Context, token string, newExpiresIn time.Duration) error GetSessionStats(ctx context.Context) (map[string]interface{}, error) HashTokenForMiddleware(token string) string } // AuditServiceInterfaceForSession defines methods needed for audit operations in session handler // Note: LogSessionRevocation may not be implemented in AuditService, so it's optional type AuditServiceInterfaceForSession interface { LogPasswordResetRequest(ctx context.Context, userID *uuid.UUID, email, ip, userAgent string) error // From AuditServiceInterface LogPasswordReset(ctx context.Context, userID uuid.UUID, success bool, ip, userAgent string) error // From AuditServiceInterface } // SessionHandler gère les opérations sur les sessions type SessionHandler struct { sessionService SessionServiceInterfaceForSession auditService AuditServiceInterfaceForSession logger *zap.Logger } // NewSessionHandler crée un nouveau handler de session func NewSessionHandler( sessionService *services.SessionService, auditService *services.AuditService, logger *zap.Logger, ) *SessionHandler { // Wrap concrete services to match interfaces return &SessionHandler{ sessionService: &sessionServiceWrapper{sessionService: sessionService}, auditService: &auditServiceWrapper{auditService: auditService}, logger: logger, } } // NewSessionHandlerWithInterface creates a new session handler with interfaces (for testing) func NewSessionHandlerWithInterface( sessionService SessionServiceInterfaceForSession, auditService AuditServiceInterfaceForSession, logger *zap.Logger, ) *SessionHandler { return &SessionHandler{ sessionService: sessionService, auditService: auditService, logger: logger, } } // sessionServiceWrapper wraps *services.SessionService to implement SessionServiceInterfaceForSession type sessionServiceWrapper struct { sessionService *services.SessionService } func (w *sessionServiceWrapper) RevokeSession(ctx context.Context, token string) error { return w.sessionService.RevokeSession(ctx, token) } func (w *sessionServiceWrapper) RevokeAllUserSessions(ctx context.Context, userID uuid.UUID) (int64, error) { return w.sessionService.RevokeAllUserSessions(ctx, userID) } func (w *sessionServiceWrapper) RevokeOtherUserSessions(ctx context.Context, userID uuid.UUID, exceptTokenHash string) (int64, error) { return w.sessionService.RevokeOtherUserSessions(ctx, userID, exceptTokenHash) } func (w *sessionServiceWrapper) GetUserSessions(ctx context.Context, userID uuid.UUID) ([]*services.Session, error) { return w.sessionService.GetUserSessions(ctx, userID) } func (w *sessionServiceWrapper) DeleteSession(ctx context.Context, tokenHash string) error { return w.sessionService.DeleteSession(ctx, tokenHash) } func (w *sessionServiceWrapper) RefreshSession(ctx context.Context, token string, newExpiresIn time.Duration) error { return w.sessionService.RefreshSession(ctx, token, newExpiresIn) } func (w *sessionServiceWrapper) GetSessionStats(ctx context.Context) (map[string]interface{}, error) { return w.sessionService.GetSessionStats(ctx) } func (w *sessionServiceWrapper) HashTokenForMiddleware(token string) string { return w.sessionService.HashTokenForMiddleware(token) } // auditServiceWrapper wraps *services.AuditService to implement AuditServiceInterfaceForSession type auditServiceWrapper struct { auditService *services.AuditService } func (w *auditServiceWrapper) LogPasswordResetRequest(ctx context.Context, userID *uuid.UUID, email, ip, userAgent string) error { return w.auditService.LogPasswordResetRequest(ctx, userID, email, ip, userAgent) } func (w *auditServiceWrapper) LogPasswordReset(ctx context.Context, userID uuid.UUID, success bool, ip, userAgent string) error { return w.auditService.LogPasswordReset(ctx, userID, success, ip, userAgent) } // LogSessionRevocation is not implemented in AuditService, so we skip it // Logout gère la déconnexion d'un utilisateur func (sh *SessionHandler) Logout() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userIDInterface, exists := c.Get("user_id") if !exists { RespondWithAppError(c, apperrors.NewUnauthorizedError("User not authenticated")) return } var userID uuid.UUID switch v := userIDInterface.(type) { case uuid.UUID: userID = v case string: var err error userID, err = uuid.Parse(v) if err != nil { RespondWithAppError(c, apperrors.NewInternalError("Invalid user ID format")) return } default: RespondWithAppError(c, apperrors.NewInternalError("Invalid user ID type")) return } // Récupérer le token depuis le header Authorization authHeader := c.GetHeader("Authorization") if authHeader == "" { RespondWithAppError(c, apperrors.NewValidationError("Authorization header required")) return } // Extraire le token tokenParts := strings.Split(authHeader, " ") if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { RespondWithAppError(c, apperrors.NewValidationError("Invalid Authorization header format")) return } tokenString := tokenParts[1] // Révoquer la session err := sh.sessionService.RevokeSession(c.Request.Context(), tokenString) if err != nil { sh.logger.Error("Failed to revoke session", zap.Error(err), zap.String("user_id", userID.String()), ) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to logout", err)) return } sh.logger.Info("User logged out", zap.String("user_id", userID.String()), zap.String("ip", c.ClientIP()), ) RespondSuccess(c, http.StatusOK, gin.H{ "message": "Logged out successfully", }) } } // LogoutAll gère la déconnexion de toutes les sessions d'un utilisateur func (sh *SessionHandler) LogoutAll() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userIDInterface, exists := c.Get("user_id") if !exists { RespondWithAppError(c, apperrors.NewUnauthorizedError("User not authenticated")) return } var userID uuid.UUID switch v := userIDInterface.(type) { case uuid.UUID: userID = v case string: var err error userID, err = uuid.Parse(v) if err != nil { RespondWithAppError(c, apperrors.NewInternalError("Invalid user ID format")) return } default: RespondWithAppError(c, apperrors.NewInternalError("Invalid user ID type")) return } // Révoquer toutes les sessions revokedCount, err := sh.sessionService.RevokeAllUserSessions(c.Request.Context(), userID) if err != nil { sh.logger.Error("Failed to revoke all user sessions", zap.Error(err), zap.String("user_id", userID.String()), ) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to logout all sessions", err)) return } sh.logger.Info("All user sessions revoked", zap.String("user_id", userID.String()), zap.Int64("sessions_revoked", revokedCount), zap.String("ip", c.ClientIP()), ) RespondSuccess(c, http.StatusOK, gin.H{ "message": "All sessions logged out successfully", "sessions_revoked": revokedCount, }) } } // LogoutOthers révoque toutes les sessions sauf la session courante. // POST /sessions/logout-others func (sh *SessionHandler) LogoutOthers() gin.HandlerFunc { return func(c *gin.Context) { userIDInterface, exists := c.Get("user_id") if !exists { RespondWithAppError(c, apperrors.NewUnauthorizedError("User not authenticated")) return } var userID uuid.UUID switch v := userIDInterface.(type) { case uuid.UUID: userID = v case string: var err error userID, err = uuid.Parse(v) if err != nil { RespondWithAppError(c, apperrors.NewInternalError("Invalid user ID format")) return } default: RespondWithAppError(c, apperrors.NewInternalError("Invalid user ID type")) return } var currentTokenHash string authHeader := c.GetHeader("Authorization") if authHeader != "" { parts := strings.Split(authHeader, " ") if len(parts) == 2 && parts[0] == "Bearer" { currentTokenHash = sh.sessionService.HashTokenForMiddleware(parts[1]) } } revokedCount, err := sh.sessionService.RevokeOtherUserSessions(c.Request.Context(), userID, currentTokenHash) if err != nil { sh.logger.Error("Failed to revoke other sessions", zap.Error(err), zap.String("user_id", userID.String()), ) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to revoke other sessions", err)) return } RespondSuccess(c, http.StatusOK, gin.H{ "message": "Other sessions revoked successfully", "sessions_revoked": revokedCount, }) } } // GetSessions récupère toutes les sessions actives d'un utilisateur func (sh *SessionHandler) GetSessions() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userIDInterface, exists := c.Get("user_id") if !exists { RespondWithAppError(c, apperrors.NewUnauthorizedError("User not authenticated")) return } var userID uuid.UUID switch v := userIDInterface.(type) { case uuid.UUID: userID = v case string: var err error userID, err = uuid.Parse(v) if err != nil { RespondWithAppError(c, apperrors.NewInternalError("Invalid user ID format")) return } default: RespondWithAppError(c, apperrors.NewInternalError("Invalid user ID type")) return } // INT-017: Récupérer le token actuel pour identifier la session courante var currentTokenHash string authHeader := c.GetHeader("Authorization") if authHeader != "" { tokenParts := strings.Split(authHeader, " ") if len(tokenParts) == 2 && tokenParts[0] == "Bearer" { currentTokenHash = sh.sessionService.HashTokenForMiddleware(tokenParts[1]) } } // Récupérer les sessions sessions, err := sh.sessionService.GetUserSessions(c.Request.Context(), userID) if err != nil { sh.logger.Error("Failed to get user sessions", zap.Error(err), zap.String("user_id", userID.String()), ) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get sessions", err)) return } // INT-017: Formater les sessions pour la réponse avec identification de la session actuelle var sessionList []map[string]interface{} for _, session := range sessions { isCurrent := currentTokenHash != "" && session.TokenHash == currentTokenHash sessionData := map[string]interface{}{ "id": session.ID, "created_at": session.CreatedAt, "expires_at": session.ExpiresAt, "ip_address": session.IPAddress, "user_agent": session.UserAgent, "is_current": isCurrent, } sessionList = append(sessionList, sessionData) } RespondSuccess(c, http.StatusOK, gin.H{ "sessions": sessionList, "count": len(sessionList), }) } } // RevokeSession révoque une session spécifique func (sh *SessionHandler) RevokeSession() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userIDInterface, exists := c.Get("user_id") if !exists { RespondWithAppError(c, apperrors.NewUnauthorizedError("User not authenticated")) return } var userID uuid.UUID switch v := userIDInterface.(type) { case uuid.UUID: userID = v case string: var err error userID, err = uuid.Parse(v) if err != nil { RespondWithAppError(c, apperrors.NewInternalError("Invalid user ID format")) return } default: RespondWithAppError(c, apperrors.NewInternalError("Invalid user ID type")) return } // Récupérer l'ID de session depuis les paramètres (UUID) sessionIDStr := c.Param("session_id") sessionID, err := uuid.Parse(sessionIDStr) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("Invalid session ID")) return } // Récupérer les sessions de l'utilisateur pour vérifier la propriété sessions, err := sh.sessionService.GetUserSessions(c.Request.Context(), userID) if err != nil { sh.logger.Error("Failed to get user sessions", zap.Error(err), zap.String("user_id", userID.String()), ) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get sessions", err)) return } // Vérifier que la session appartient à l'utilisateur sessionFound := false var targetSession *services.Session for _, session := range sessions { if session.ID == sessionID { sessionFound = true targetSession = session break } } if !sessionFound { RespondWithAppError(c, apperrors.NewNotFoundError("Session")) return } if targetSession != nil { // Revoke by Hash using DeleteSession err = sh.sessionService.DeleteSession(c.Request.Context(), targetSession.TokenHash) if err != nil { sh.logger.Error("Failed to revoke session", zap.Error(err), zap.String("user_id", userID.String()), ) RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to revoke session", err)) return } } sh.logger.Info("Session revoked", zap.String("user_id", userID.String()), zap.String("session_id", sessionID.String()), zap.String("ip", c.ClientIP()), ) RespondSuccess(c, http.StatusOK, gin.H{ "message": "Session revoked successfully", }) } } // GetSessionStats récupère les statistiques des sessions // GET /api/v1/sessions/stats // BE-API-031: Implement session stats endpoint func (sh *SessionHandler) GetSessionStats() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Récupérer les statistiques stats, err := sh.sessionService.GetSessionStats(c.Request.Context()) if err != nil { sh.logger.Error("Failed to get session stats", zap.Error(err), zap.String("user_id", userID.String()), ) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get session stats", err)) return } // BE-API-031: Standardize response format RespondSuccess(c, http.StatusOK, gin.H{ "user_id": userID, "stats": stats, }) } } // RefreshSession rafraîchit une session // POST /api/v1/sessions/refresh // BE-API-030: Implement session refresh endpoint validation func (sh *SessionHandler) RefreshSession() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Récupérer le token depuis le header Authorization authHeader := c.GetHeader("Authorization") if authHeader == "" { RespondWithAppError(c, apperrors.NewValidationError("Authorization header required")) return } // Extraire le token tokenParts := strings.Split(authHeader, " ") if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { RespondWithAppError(c, apperrors.NewValidationError("Invalid Authorization header format")) return } tokenString := tokenParts[1] // Rafraîchir la session newExpiresIn := 24 * time.Hour // 24 heures err := sh.sessionService.RefreshSession(c.Request.Context(), tokenString, newExpiresIn) if err != nil { sh.logger.Error("Failed to refresh session", zap.Error(err), zap.String("user_id", userID.String()), ) // Vérifier si c'est une erreur de session non trouvée ou expirée if strings.Contains(err.Error(), "session not found") || strings.Contains(err.Error(), "expired") { RespondWithAppError(c, apperrors.NewUnauthorizedError("Session expired or invalid")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to refresh session", err)) return } sh.logger.Info("Session refreshed", zap.String("user_id", userID.String()), zap.String("ip", c.ClientIP()), ) // BE-API-030: Standardize response format RespondSuccess(c, http.StatusOK, gin.H{ "message": "Session refreshed successfully", "expires_in": newExpiresIn.Seconds(), "expires_at": time.Now().Add(newExpiresIn), }) } }