veza/veza-backend-api/internal/handlers/session.go
senke d294245761 [BE-API-031] be-api: Implement session stats endpoint
- Standardized GetSessionStats handler to use RespondSuccess/RespondWithAppError
- Replaced c.Get with GetUserIDUUID helper
- Handler retrieves session statistics via SessionService.GetSessionStats
- Handler returns total_active sessions and unique_users count
- Handler uses standard API response format

Phase: PHASE-2
Priority: P2
Progress: 38/267 (14.2%)
2025-12-24 11:48:43 +01:00

380 lines
11 KiB
Go

package handlers
import (
"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"
)
// SessionHandler gère les opérations sur les sessions
type SessionHandler struct {
sessionService *services.SessionService
auditService *services.AuditService
logger *zap.Logger
}
// NewSessionHandler crée un nouveau handler de session
func NewSessionHandler(
sessionService *services.SessionService,
auditService *services.AuditService,
logger *zap.Logger,
) *SessionHandler {
return &SessionHandler{
sessionService: sessionService,
auditService: auditService,
logger: logger,
}
}
// 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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "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 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID format"})
return
}
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
// Récupérer le token depuis le header Authorization
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Authorization header required"})
return
}
// Extraire le token
tokenParts := strings.Split(authHeader, " ")
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
c.JSON(http.StatusBadRequest, gin.H{"error": "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()),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to logout"})
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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "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 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID format"})
return
}
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "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()),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to logout all sessions"})
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,
})
}
}
// 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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "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 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID format"})
return
}
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
// Récupérer les sessions
sessions, err := sh.sessionService.GetUserSessions(userID)
if err != nil {
sh.logger.Error("Failed to get user sessions",
zap.Error(err),
zap.String("user_id", userID.String()),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get sessions"})
return
}
// Formater les sessions pour la réponse
var sessionList []map[string]interface{}
for _, session := range sessions {
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": false, // TODO: Déterminer si c'est la session actuelle
}
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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "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 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID format"})
return
}
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "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 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid session ID"})
return
}
// Récupérer les sessions de l'utilisateur pour vérifier la propriété
sessions, err := sh.sessionService.GetUserSessions(userID)
if err != nil {
sh.logger.Error("Failed to get user sessions",
zap.Error(err),
zap.String("user_id", userID.String()),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get sessions"})
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 {
c.JSON(http.StatusNotFound, gin.H{"error": "Session not found"})
return
}
if targetSession != nil {
// Revoke by Hash using DeleteSession
err = sh.sessionService.DeleteSession(targetSession.TokenHash)
if err != nil {
sh.logger.Error("Failed to revoke session",
zap.Error(err),
zap.String("user_id", userID.String()),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to revoke session"})
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),
})
}
}