veza/veza-backend-api/internal/handlers/auth.go
okinrev b7955a680c P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.

Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.

Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).

Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.

Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 11:14:38 +01:00

284 lines
8.7 KiB
Go

package handlers
import (
"net/http"
"strings"
"time"
"veza-backend-api/internal/core/auth"
"veza-backend-api/internal/dto"
apperrors "veza-backend-api/internal/errors"
// "veza-backend-api/internal/response" // Removed this import
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// Login gère la connexion des utilisateurs
// T0203: Intègre création de session après login avec IP et User-Agent
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
func Login(authService *auth.AuthService, sessionService *services.SessionService, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
var req dto.LoginRequest
if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
// req.RememberMe is a bool, not *bool, so no need to check for nil or indirect
rememberMe := req.RememberMe
user, tokens, err := authService.Login(c.Request.Context(), req.Email, req.Password, rememberMe)
if err != nil {
if strings.Contains(err.Error(), "email not verified") {
c.JSON(http.StatusForbidden, gin.H{
"error": err.Error(),
"code": "EMAIL_NOT_VERIFIED",
})
return
}
if strings.Contains(err.Error(), "invalid credentials") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to authenticate"})
return
}
if sessionService != nil {
ipAddress := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
if userAgent == "" {
userAgent = "Unknown"
}
expiresIn := 30 * 24 * time.Hour
if rememberMe {
expiresIn = 90 * 24 * time.Hour
}
sessionReq := &services.SessionCreateRequest{
UserID: user.ID,
Token: tokens.AccessToken,
IPAddress: ipAddress,
UserAgent: userAgent,
ExpiresIn: expiresIn,
}
if _, err := sessionService.CreateSession(c.Request.Context(), sessionReq); err != nil {
if logger != nil {
logger.Warn("Failed to create session after login",
zap.String("user_id", user.ID.String()),
zap.String("ip_address", ipAddress),
zap.Error(err),
)
}
}
}
c.JSON(http.StatusOK, dto.LoginResponse{
User: dto.UserResponse{
ID: user.ID,
Email: user.Email,
},
Token: dto.TokenResponse{
AccessToken: tokens.AccessToken,
RefreshToken: tokens.RefreshToken,
ExpiresIn: int(authService.JWTService.Config.AccessTokenTTL.Seconds()),
},
})
}
}
// Register gère l'inscription des utilisateurs
// GO-013: Utilise validator centralisé pour validation améliorée
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
func Register(authService *auth.AuthService, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
var req dto.RegisterRequest
if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
user, err := authService.Register(c.Request.Context(), req.Email, req.Password)
if err != nil {
switch {
case services.IsUserAlreadyExistsError(err):
c.JSON(http.StatusConflict, gin.H{"error": "User already exists"})
case services.IsInvalidEmail(err):
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email format"})
case services.IsWeakPassword(err):
c.JSON(http.StatusBadRequest, gin.H{"error": "Password does not meet requirements"})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
}
return
}
c.JSON(http.StatusCreated, dto.RegisterResponse{
User: dto.UserResponse{
ID: user.ID,
Email: user.Email,
Username: user.Username,
},
})
}
}
// Refresh gère le rafraîchissement d'un access token
// GO-013: Utilise validator centralisé pour validation améliorée
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
func Refresh(authService *auth.AuthService, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
var req dto.RefreshRequest
if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
tokens, err := authService.Refresh(c.Request.Context(), req.RefreshToken)
if err != nil {
if strings.Contains(err.Error(), "invalid refresh token") ||
strings.Contains(err.Error(), "not found") ||
strings.Contains(err.Error(), "expired") ||
strings.Contains(err.Error(), "token version mismatch") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid refresh token"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh token"})
return
}
c.JSON(http.StatusOK, dto.TokenResponse{
AccessToken: tokens.AccessToken,
RefreshToken: tokens.RefreshToken,
ExpiresIn: int(authService.JWTService.Config.AccessTokenTTL.Seconds()), // Use JWT config
})
}
}
// Logout gère la déconnexion des utilisateurs
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
func Logout(authService *auth.AuthService, sessionService *services.SessionService, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
userIDInterface, exists := c.Get("user_id")
if !exists {
RespondWithAppError(c, apperrors.NewUnauthorizedError("User not authenticated"))
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "Invalid user ID type in context"))
return
}
var req struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
if err := authService.Logout(c.Request.Context(), userID, req.RefreshToken); err != nil {
// Log the error but don't fail the request to prevent leaking info
}
if sessionService != nil {
authHeader := c.GetHeader("Authorization")
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
token := strings.TrimPrefix(authHeader, "Bearer ")
if err := sessionService.RevokeSession(c.Request.Context(), token); err != nil {
// Log the error but don't fail the request
}
}
}
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
}
}
// VerifyEmail gère la vérification de l'email
func VerifyEmail(authService *auth.AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Query("token")
if token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Token required"})
return
}
if err := authService.VerifyEmail(c.Request.Context(), token); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Email verified successfully"})
}
}
// ResendVerification gère la demande de renvoi d'email de vérification
// P0: JSON Hardening - Utilise BindAndValidateJSON pour une gestion robuste des erreurs
func ResendVerification(authService *auth.AuthService, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
commonHandler := NewCommonHandler(logger)
var req dto.ResendVerificationRequest
if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
if err := authService.ResendVerificationEmail(c.Request.Context(), req.Email); err != nil {
if strings.Contains(err.Error(), "email already verified") {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"message": "Verification email sent if account exists"})
}
}
// CheckUsername vérifie la disponibilité d'un nom d'utilisateur
func CheckUsername(authService *auth.AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
username := c.Query("username")
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
return
}
_, err := authService.GetUserByUsername(c.Request.Context(), username)
available := err != nil
c.JSON(http.StatusOK, gin.H{
"available": available,
"username": username,
})
}
}
// GetMe retourne les informations de l'utilisateur connecté
func GetMe() gin.HandlerFunc {
return func(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{
"id": userID,
"email": c.GetString("email"),
"role": c.GetString("role"),
})
}
}