package auth import ( "net/http" "strings" "time" "veza-backend-api/internal/dto" "veza-backend-api/internal/response" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // AuthHandler gère les requêtes d'authentification pour T0151 type AuthHandler struct { authService *AuthService // Changed to *AuthService (from the current package) sessionService *services.SessionService logger *zap.Logger } // NewAuthHandler crée une nouvelle instance d'AuthHandler func NewAuthHandler(authService *AuthService, sessionService *services.SessionService, logger *zap.Logger) *AuthHandler { // Changed to *AuthService return &AuthHandler{ authService: authService, sessionService: sessionService, logger: logger, } } // Register gère l'inscription d'un nouvel utilisateur func (h *AuthHandler) Register(c *gin.Context) { var req dto.RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { errorMsg := err.Error() if strings.Contains(errorMsg, "Password") && strings.Contains(errorMsg, "min") { errorMsg = "Le mot de passe doit contenir au moins 12 caractères" } else if strings.Contains(errorMsg, "PasswordConfirm") && strings.Contains(errorMsg, "eqfield") { errorMsg = "Les mots de passe ne correspondent pas" } else if strings.Contains(errorMsg, "Email") && strings.Contains(errorMsg, "email") { errorMsg = "Format d'email invalide" } else if strings.Contains(errorMsg, "required") { if strings.Contains(errorMsg, "Password") { errorMsg = "Le mot de passe est requis" } else if strings.Contains(errorMsg, "Email") { errorMsg = "L'email est requis" } else if strings.Contains(errorMsg, "PasswordConfirm") { errorMsg = "La confirmation du mot de passe est requise" } } h.logger.Warn("Invalid registration request", zap.Error(err), zap.String("error_message", errorMsg)) // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusBadRequest, errorMsg) return } h.logger.Info("Received registration request", zap.Any("req", req)) user, tokens, err := h.authService.Register(c.Request.Context(), req.Email, req.Username, req.Password) if err != nil { if strings.Contains(err.Error(), "already exists") { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusConflict, err.Error()) return } if strings.Contains(err.Error(), "validation") || strings.Contains(err.Error(), "invalid") { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusBadRequest, err.Error()) return } // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusInternalServerError, "Failed to create user") return } // Construire la réponse avec les tokens générés response := dto.RegisterResponse{ User: dto.UserResponse{ ID: user.ID, Email: user.Email, Username: user.Username, }, Token: dto.TokenResponse{ AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, ExpiresIn: tokens.ExpiresIn, }, } c.JSON(http.StatusCreated, response) } // Login gère la connexion d'un utilisateur func (h *AuthHandler) Login(c *gin.Context) { var req dto.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusBadRequest, err.Error()) return } user, tokens, err := h.authService.Login(c.Request.Context(), req.Email, req.Password, req.RememberMe) if err != nil { if strings.Contains(err.Error(), "email not verified") { // MOD-P2-003: Utiliser AppError au lieu de gin.H (403 -> ErrCodeForbidden) response.Error(c, http.StatusForbidden, err.Error()) return } if strings.Contains(err.Error(), "invalid credentials") { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusUnauthorized, "Invalid credentials") return } // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusInternalServerError, "Failed to authenticate") return } if h.sessionService != nil { ipAddress := c.ClientIP() userAgent := c.GetHeader("User-Agent") if userAgent == "" { userAgent = "Unknown" } expiresIn := 30 * 24 * time.Hour if req.RememberMe { expiresIn = 90 * 24 * time.Hour } sessionReq := &services.SessionCreateRequest{ UserID: user.ID, Token: tokens.AccessToken, IPAddress: ipAddress, UserAgent: userAgent, ExpiresIn: expiresIn, } if _, err := h.sessionService.CreateSession(c.Request.Context(), sessionReq); err != nil { h.logger.Warn("Failed to create session after login", zap.String("user_id", user.ID.String()), zap.String("ip_address", ipAddress), zap.Error(err), ) } } response := dto.LoginResponse{ User: dto.UserResponse{ ID: user.ID, Email: user.Email, }, Token: dto.TokenResponse{ AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, ExpiresIn: int(h.authService.JWTService.GetConfig().AccessTokenTTL.Seconds()), }, } c.JSON(http.StatusOK, response) } // Refresh gère le rafraîchissement d'un access token func (h *AuthHandler) Refresh(c *gin.Context) { var req dto.RefreshRequest if err := c.ShouldBindJSON(&req); err != nil { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusBadRequest, err.Error()) return } tokens, err := h.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") { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusUnauthorized, "Invalid refresh token") return } // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusInternalServerError, "Failed to refresh token") return } response := dto.TokenResponse{ AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, ExpiresIn: 900, } c.JSON(http.StatusOK, response) } // CheckUsername vérifie la disponibilité d'un nom d'utilisateur func (h *AuthHandler) CheckUsername(c *gin.Context) { username := c.Query("username") if username == "" { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusBadRequest, "Username is required") return } _, err := h.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 (h *AuthHandler) GetMe(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusUnauthorized, "Unauthorized") return } c.JSON(http.StatusOK, gin.H{ "id": userID, "email": c.GetString("email"), "role": c.GetString("role"), }) } // Logout déconnecte l'utilisateur func (h *AuthHandler) Logout(c *gin.Context) { userIDInterface, exists := c.Get("user_id") if !exists { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusUnauthorized, "Unauthorized") return } userID, ok := userIDInterface.(uuid.UUID) if !ok { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusInternalServerError, "Invalid user ID type in context") return } var req struct { RefreshToken string `json:"refresh_token" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusBadRequest, "Refresh token is required") return } if err := h.authService.Logout(c.Request.Context(), userID, req.RefreshToken); err != nil { h.logger.Error("Failed to logout (revoke token)", zap.Error(err)) } if h.sessionService != nil { authHeader := c.GetHeader("Authorization") if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") { token := strings.TrimPrefix(authHeader, "Bearer ") if err := h.sessionService.RevokeSession(c.Request.Context(), token); err != nil { h.logger.Warn("Failed to revoke session on logout", zap.Error(err)) } } } c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"}) } // VerifyEmail gère la vérification de l'email func (h *AuthHandler) VerifyEmail(c *gin.Context) { token := c.Query("token") if token == "" { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusBadRequest, "Token required") return } if err := h.authService.VerifyEmail(c.Request.Context(), token); err != nil { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusBadRequest, 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 func (h *AuthHandler) ResendVerification(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` } if err := c.ShouldBindJSON(&req); err != nil { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusBadRequest, err.Error()) return } if err := h.authService.ResendVerificationEmail(c.Request.Context(), req.Email); err != nil { if err.Error() == "email already verified" { // MOD-P2-003: Utiliser AppError au lieu de gin.H response.Error(c, http.StatusBadRequest, err.Error()) return } } c.JSON(http.StatusOK, gin.H{"message": "Verification email sent if account exists"}) } // GetUserByUsername gets a user by username func (h *AuthHandler) GetUserByUsername(c *gin.Context) { username := c.Param("username") user, err := h.authService.GetUserByUsername(c.Request.Context(), username) if err != nil { response.NotFound(c, "User not found") return } response.Success(c, user) }