package handlers import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "github.com/treytartt/casera-api/internal/dto/requests" "github.com/treytartt/casera-api/internal/dto/responses" "github.com/treytartt/casera-api/internal/i18n" "github.com/treytartt/casera-api/internal/middleware" "github.com/treytartt/casera-api/internal/services" ) // AuthHandler handles authentication endpoints type AuthHandler struct { authService *services.AuthService emailService *services.EmailService cache *services.CacheService appleAuthService *services.AppleAuthService } // NewAuthHandler creates a new auth handler func NewAuthHandler(authService *services.AuthService, emailService *services.EmailService, cache *services.CacheService) *AuthHandler { return &AuthHandler{ authService: authService, emailService: emailService, cache: cache, } } // SetAppleAuthService sets the Apple auth service (called after initialization) func (h *AuthHandler) SetAppleAuthService(appleAuth *services.AppleAuthService) { h.appleAuthService = appleAuth } // Login handles POST /api/auth/login/ func (h *AuthHandler) Login(c *gin.Context) { var req requests.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, responses.ErrorResponse{ Error: i18n.LocalizedMessage(c, "error.invalid_request_body"), Details: map[string]string{ "validation": err.Error(), }, }) return } response, err := h.authService.Login(&req) if err != nil { status := http.StatusUnauthorized message := i18n.LocalizedMessage(c, "error.invalid_credentials") if errors.Is(err, services.ErrUserInactive) { message = i18n.LocalizedMessage(c, "error.account_inactive") } log.Debug().Err(err).Str("identifier", req.Username).Msg("Login failed") c.JSON(status, responses.ErrorResponse{Error: message}) return } c.JSON(http.StatusOK, response) } // Register handles POST /api/auth/register/ func (h *AuthHandler) Register(c *gin.Context) { var req requests.RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, responses.ErrorResponse{ Error: i18n.LocalizedMessage(c, "error.invalid_request_body"), Details: map[string]string{ "validation": err.Error(), }, }) return } response, confirmationCode, err := h.authService.Register(&req) if err != nil { status := http.StatusBadRequest message := err.Error() if errors.Is(err, services.ErrUsernameTaken) { message = i18n.LocalizedMessage(c, "error.username_taken") } else if errors.Is(err, services.ErrEmailTaken) { message = i18n.LocalizedMessage(c, "error.email_taken") } else { status = http.StatusInternalServerError message = i18n.LocalizedMessage(c, "error.registration_failed") log.Error().Err(err).Msg("Registration failed") } c.JSON(status, responses.ErrorResponse{Error: message}) return } // Send welcome email with confirmation code (async) if h.emailService != nil && confirmationCode != "" { go func() { if err := h.emailService.SendWelcomeEmail(req.Email, req.FirstName, confirmationCode); err != nil { log.Error().Err(err).Str("email", req.Email).Msg("Failed to send welcome email") } }() } c.JSON(http.StatusCreated, response) } // Logout handles POST /api/auth/logout/ func (h *AuthHandler) Logout(c *gin.Context) { token := middleware.GetAuthToken(c) if token == "" { c.JSON(http.StatusUnauthorized, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.not_authenticated")}) return } // Invalidate token in database if err := h.authService.Logout(token); err != nil { log.Warn().Err(err).Msg("Failed to delete token from database") } // Invalidate token in cache if h.cache != nil { if err := h.cache.InvalidateAuthToken(c.Request.Context(), token); err != nil { log.Warn().Err(err).Msg("Failed to invalidate token in cache") } } c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.logged_out")}) } // CurrentUser handles GET /api/auth/me/ func (h *AuthHandler) CurrentUser(c *gin.Context) { user := middleware.MustGetAuthUser(c) if user == nil { return } response, err := h.authService.GetCurrentUser(user.ID) if err != nil { log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to get current user") c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.failed_to_get_user")}) return } c.JSON(http.StatusOK, response) } // UpdateProfile handles PUT/PATCH /api/auth/profile/ func (h *AuthHandler) UpdateProfile(c *gin.Context) { user := middleware.MustGetAuthUser(c) if user == nil { return } var req requests.UpdateProfileRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, responses.ErrorResponse{ Error: i18n.LocalizedMessage(c, "error.invalid_request_body"), Details: map[string]string{ "validation": err.Error(), }, }) return } response, err := h.authService.UpdateProfile(user.ID, &req) if err != nil { if errors.Is(err, services.ErrEmailTaken) { c.JSON(http.StatusBadRequest, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.email_already_taken")}) return } log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to update profile") c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.failed_to_update_profile")}) return } c.JSON(http.StatusOK, response) } // VerifyEmail handles POST /api/auth/verify-email/ func (h *AuthHandler) VerifyEmail(c *gin.Context) { user := middleware.MustGetAuthUser(c) if user == nil { return } var req requests.VerifyEmailRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, responses.ErrorResponse{ Error: i18n.LocalizedMessage(c, "error.invalid_request_body"), Details: map[string]string{ "validation": err.Error(), }, }) return } err := h.authService.VerifyEmail(user.ID, req.Code) if err != nil { status := http.StatusBadRequest message := err.Error() if errors.Is(err, services.ErrInvalidCode) { message = i18n.LocalizedMessage(c, "error.invalid_verification_code") } else if errors.Is(err, services.ErrCodeExpired) { message = i18n.LocalizedMessage(c, "error.verification_code_expired") } else if errors.Is(err, services.ErrAlreadyVerified) { message = i18n.LocalizedMessage(c, "error.email_already_verified") } else { status = http.StatusInternalServerError message = i18n.LocalizedMessage(c, "error.verification_failed") log.Error().Err(err).Uint("user_id", user.ID).Msg("Email verification failed") } c.JSON(status, responses.ErrorResponse{Error: message}) return } c.JSON(http.StatusOK, responses.VerifyEmailResponse{ Message: i18n.LocalizedMessage(c, "message.email_verified"), Verified: true, }) } // ResendVerification handles POST /api/auth/resend-verification/ func (h *AuthHandler) ResendVerification(c *gin.Context) { user := middleware.MustGetAuthUser(c) if user == nil { return } code, err := h.authService.ResendVerificationCode(user.ID) if err != nil { if errors.Is(err, services.ErrAlreadyVerified) { c.JSON(http.StatusBadRequest, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.email_already_verified")}) return } log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to resend verification") c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.failed_to_resend_verification")}) return } // Send verification email (async) if h.emailService != nil { go func() { if err := h.emailService.SendVerificationEmail(user.Email, user.FirstName, code); err != nil { log.Error().Err(err).Str("email", user.Email).Msg("Failed to send verification email") } }() } c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.verification_email_sent")}) } // ForgotPassword handles POST /api/auth/forgot-password/ func (h *AuthHandler) ForgotPassword(c *gin.Context) { var req requests.ForgotPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, responses.ErrorResponse{ Error: i18n.LocalizedMessage(c, "error.invalid_request_body"), Details: map[string]string{ "validation": err.Error(), }, }) return } code, user, err := h.authService.ForgotPassword(req.Email) if err != nil { if errors.Is(err, services.ErrRateLimitExceeded) { c.JSON(http.StatusTooManyRequests, responses.ErrorResponse{ Error: i18n.LocalizedMessage(c, "error.rate_limit_exceeded"), }) return } log.Error().Err(err).Str("email", req.Email).Msg("Forgot password failed") // Don't reveal errors to prevent email enumeration } // Send password reset email (async) - only if user found if h.emailService != nil && code != "" && user != nil { go func() { if err := h.emailService.SendPasswordResetEmail(user.Email, user.FirstName, code); err != nil { log.Error().Err(err).Str("email", user.Email).Msg("Failed to send password reset email") } }() } // Always return success to prevent email enumeration c.JSON(http.StatusOK, responses.ForgotPasswordResponse{ Message: i18n.LocalizedMessage(c, "message.password_reset_email_sent"), }) } // VerifyResetCode handles POST /api/auth/verify-reset-code/ func (h *AuthHandler) VerifyResetCode(c *gin.Context) { var req requests.VerifyResetCodeRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, responses.ErrorResponse{ Error: i18n.LocalizedMessage(c, "error.invalid_request_body"), Details: map[string]string{ "validation": err.Error(), }, }) return } resetToken, err := h.authService.VerifyResetCode(req.Email, req.Code) if err != nil { status := http.StatusBadRequest message := i18n.LocalizedMessage(c, "error.invalid_verification_code") if errors.Is(err, services.ErrCodeExpired) { message = i18n.LocalizedMessage(c, "error.verification_code_expired") } else if errors.Is(err, services.ErrRateLimitExceeded) { status = http.StatusTooManyRequests message = i18n.LocalizedMessage(c, "error.too_many_attempts") } c.JSON(status, responses.ErrorResponse{Error: message}) return } c.JSON(http.StatusOK, responses.VerifyResetCodeResponse{ Message: i18n.LocalizedMessage(c, "message.reset_code_verified"), ResetToken: resetToken, }) } // ResetPassword handles POST /api/auth/reset-password/ func (h *AuthHandler) ResetPassword(c *gin.Context) { var req requests.ResetPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, responses.ErrorResponse{ Error: i18n.LocalizedMessage(c, "error.invalid_request_body"), Details: map[string]string{ "validation": err.Error(), }, }) return } err := h.authService.ResetPassword(req.ResetToken, req.NewPassword) if err != nil { status := http.StatusBadRequest message := i18n.LocalizedMessage(c, "error.invalid_reset_token") if errors.Is(err, services.ErrInvalidResetToken) { message = i18n.LocalizedMessage(c, "error.invalid_reset_token") } else { status = http.StatusInternalServerError message = i18n.LocalizedMessage(c, "error.password_reset_failed") log.Error().Err(err).Msg("Password reset failed") } c.JSON(status, responses.ErrorResponse{Error: message}) return } c.JSON(http.StatusOK, responses.ResetPasswordResponse{ Message: i18n.LocalizedMessage(c, "message.password_reset_success"), }) } // AppleSignIn handles POST /api/auth/apple-sign-in/ func (h *AuthHandler) AppleSignIn(c *gin.Context) { var req requests.AppleSignInRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, responses.ErrorResponse{ Error: i18n.LocalizedMessage(c, "error.invalid_request_body"), Details: map[string]string{ "validation": err.Error(), }, }) return } if h.appleAuthService == nil { log.Error().Msg("Apple auth service not configured") c.JSON(http.StatusInternalServerError, responses.ErrorResponse{ Error: i18n.LocalizedMessage(c, "error.apple_signin_not_configured"), }) return } response, err := h.authService.AppleSignIn(c.Request.Context(), h.appleAuthService, &req) if err != nil { status := http.StatusUnauthorized message := i18n.LocalizedMessage(c, "error.apple_signin_failed") if errors.Is(err, services.ErrUserInactive) { message = i18n.LocalizedMessage(c, "error.account_inactive") } else if errors.Is(err, services.ErrAppleSignInFailed) { message = i18n.LocalizedMessage(c, "error.invalid_apple_token") } log.Debug().Err(err).Msg("Apple Sign In failed") c.JSON(status, responses.ErrorResponse{Error: message}) return } // Send welcome email for new users (async) if response.IsNewUser && h.emailService != nil && response.User.Email != "" { go func() { if err := h.emailService.SendAppleWelcomeEmail(response.User.Email, response.User.FirstName); err != nil { log.Error().Err(err).Str("email", response.User.Email).Msg("Failed to send Apple welcome email") } }() } c.JSON(http.StatusOK, response) }