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.).
284 lines
8.7 KiB
Go
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"),
|
|
})
|
|
}
|
|
}
|