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/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: "Invalid request body", Details: map[string]string{ "validation": err.Error(), }, }) return } response, err := h.authService.Login(&req) if err != nil { status := http.StatusUnauthorized message := "Invalid credentials" if errors.Is(err, services.ErrUserInactive) { message = "Account is 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: "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 = "Username already taken" } else if errors.Is(err, services.ErrEmailTaken) { message = "Email already registered" } else { status = http.StatusInternalServerError message = "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: "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: "Logged out successfully"}) } // 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: "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: "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: "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: "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: "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 = "Invalid verification code" } else if errors.Is(err, services.ErrCodeExpired) { message = "Verification code has expired" } else if errors.Is(err, services.ErrAlreadyVerified) { message = "Email already verified" } else { status = http.StatusInternalServerError message = "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: "Email verified successfully", 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: "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: "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: "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: "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: "Too many password reset requests. Please try again later.", }) 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: "If an account with that email exists, a password reset code has been 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: "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 := "Invalid verification code" if errors.Is(err, services.ErrCodeExpired) { message = "Verification code has expired" } else if errors.Is(err, services.ErrRateLimitExceeded) { status = http.StatusTooManyRequests message = "Too many attempts. Please request a new code." } c.JSON(status, responses.ErrorResponse{Error: message}) return } c.JSON(http.StatusOK, responses.VerifyResetCodeResponse{ Message: "Code verified successfully", 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: "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 := "Invalid or expired reset token" if errors.Is(err, services.ErrInvalidResetToken) { message = "Invalid or expired reset token" } else { status = http.StatusInternalServerError message = "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: "Password reset successfully. Please log in with your new password.", }) } // 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: "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: "Apple Sign In is not configured", }) return } response, err := h.authService.AppleSignIn(c.Request.Context(), h.appleAuthService, &req) if err != nil { status := http.StatusUnauthorized message := "Apple Sign In failed" if errors.Is(err, services.ErrUserInactive) { message = "Account is inactive" } else if errors.Is(err, services.ErrAppleSignInFailed) { message = "Invalid Apple identity token" } log.Debug().Err(err).Msg("Apple Sign In failed") c.JSON(status, responses.ErrorResponse{Error: message}) return } c.JSON(http.StatusOK, response) }