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 // @Summary User Login // @Description Authenticate user and return access/refresh tokens // @Tags Auth // @Accept json // @Produce json // @Param request body dto.LoginRequest true "Login Credentials" // @Success 200 {object} dto.LoginResponse // @Failure 400 {object} handlers.APIResponse "Validation or Bad Request" // @Failure 401 {object} handlers.APIResponse "Invalid credentials" // @Failure 500 {object} handlers.APIResponse "Internal Error" // @Router /auth/login [post] 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), ) } } } RespondSuccess(c, 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 // @Summary User Registration // @Description Register a new user account // @Tags Auth // @Accept json // @Produce json // @Param request body dto.RegisterRequest true "Registration Data" // @Success 201 {object} dto.RegisterResponse // @Failure 400 {object} handlers.APIResponse "Validation Error" // @Failure 409 {object} handlers.APIResponse "User already exists" // @Failure 500 {object} handlers.APIResponse "Internal Error" // @Router /auth/register [post] 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 } logger.Info("Received registration request (Modern)", zap.Any("req", req)) user, err := authService.Register(c.Request.Context(), req.Email, req.Username, 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: commonHandler.logger.Error("Registration failed", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) } return } RespondSuccess(c, 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 // @Summary Refresh Token // @Description Get a new access token using a refresh token // @Tags Auth // @Accept json // @Produce json // @Param request body dto.RefreshRequest true "Refresh Token" // @Success 200 {object} dto.TokenResponse // @Failure 400 {object} handlers.APIResponse "Validation Error" // @Failure 401 {object} handlers.APIResponse "Invalid/Expired Refresh Token" // @Failure 500 {object} handlers.APIResponse "Internal Error" // @Router /auth/refresh [post] 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 } RespondSuccess(c, 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 // @Summary Logout // @Description Revoke refresh token and current session // @Tags Auth // @Accept json // @Produce json // @Security BearerAuth // @Param request body object{refresh_token=string} true "Refresh Token to revoke" // @Success 200 {object} handlers.APIResponse "Success message" // @Failure 400 {object} handlers.APIResponse "Validation Error" // @Failure 401 {object} handlers.APIResponse "Unauthorized" // @Router /auth/logout [post] 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 } } } RespondSuccess(c, http.StatusOK, gin.H{"message": "Logged out successfully"}) } } // VerifyEmail gère la vérification de l'email // @Summary Verify Email // @Description Verify user email address using a token // @Tags Auth // @Accept json // @Produce json // @Param token query string true "Verification Token" // @Success 200 {object} handlers.APIResponse "Success message" // @Failure 400 {object} handlers.APIResponse "Invalid Token" // @Router /auth/verify-email [post] 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 } RespondSuccess(c, http.StatusOK, gin.H{"message": "Email verified successfully"}) } } // ResendVerification gère la demande de renvoi d'email de vérification // @Summary Resend Verification Email // @Description Resend the email verification link // @Tags Auth // @Accept json // @Produce json // @Param request body dto.ResendVerificationRequest true "Email" // @Success 200 {object} handlers.APIResponse "Success message" // @Failure 400 {object} handlers.APIResponse "Validation Error" // @Router /auth/resend-verification [post] 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 } } RespondSuccess(c, http.StatusOK, gin.H{"message": "Verification email sent if account exists"}) } } // CheckUsername vérifie la disponibilité d'un nom d'utilisateur // @Summary Check Username Availability // @Description Check if a username is already taken // @Tags Auth // @Accept json // @Produce json // @Param username query string true "Username to check" // @Success 200 {object} handlers.APIResponse{data=object{available=boolean,username=string}} // @Failure 400 {object} handlers.APIResponse "Missing Username" // @Router /auth/check-username [get] 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 RespondSuccess(c, http.StatusOK, gin.H{ "available": available, "username": username, }) } } // GetMe retourne les informations de l'utilisateur connecté // @Summary Get Current User // @Description Get profile information of the currently logged-in user // @Tags Auth // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} handlers.APIResponse{data=object{id=string,email=string,role=string}} // @Failure 401 {object} handlers.APIResponse "Unauthorized" // @Router /auth/me [get] 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 } RespondSuccess(c, http.StatusOK, gin.H{ "id": userID, "email": c.GetString("email"), "role": c.GetString("role"), }) } }