veza/veza-backend-api/internal/handlers/auth.go

358 lines
12 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
// @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"),
})
}
}