package handlers import ( "errors" "net/http" "github.com/labstack/echo/v4" "github.com/rs/zerolog/log" "github.com/treytartt/casera-api/internal/apperrors" "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" "github.com/treytartt/casera-api/internal/validator" ) // AuthHandler handles authentication endpoints type AuthHandler struct { authService *services.AuthService emailService *services.EmailService cache *services.CacheService appleAuthService *services.AppleAuthService googleAuthService *services.GoogleAuthService } // 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 } // SetGoogleAuthService sets the Google auth service (called after initialization) func (h *AuthHandler) SetGoogleAuthService(googleAuth *services.GoogleAuthService) { h.googleAuthService = googleAuth } // Login handles POST /api/auth/login/ func (h *AuthHandler) Login(c echo.Context) error { var req requests.LoginRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } response, err := h.authService.Login(&req) if err != nil { log.Debug().Err(err).Str("identifier", req.Username).Msg("Login failed") return err } return c.JSON(http.StatusOK, response) } // Register handles POST /api/auth/register/ func (h *AuthHandler) Register(c echo.Context) error { var req requests.RegisterRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } response, confirmationCode, err := h.authService.Register(&req) if err != nil { log.Debug().Err(err).Msg("Registration failed") return err } // 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") } }() } return c.JSON(http.StatusCreated, response) } // Logout handles POST /api/auth/logout/ func (h *AuthHandler) Logout(c echo.Context) error { token := middleware.GetAuthToken(c) if token == "" { return apperrors.Unauthorized("error.not_authenticated") } // 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") } } return c.JSON(http.StatusOK, responses.MessageResponse{Message: "Logged out successfully"}) } // CurrentUser handles GET /api/auth/me/ func (h *AuthHandler) CurrentUser(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } 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") return err } return c.JSON(http.StatusOK, response) } // UpdateProfile handles PUT/PATCH /api/auth/profile/ func (h *AuthHandler) UpdateProfile(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } var req requests.UpdateProfileRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } response, err := h.authService.UpdateProfile(user.ID, &req) if err != nil { log.Debug().Err(err).Uint("user_id", user.ID).Msg("Failed to update profile") return err } return c.JSON(http.StatusOK, response) } // VerifyEmail handles POST /api/auth/verify-email/ func (h *AuthHandler) VerifyEmail(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } var req requests.VerifyEmailRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } err = h.authService.VerifyEmail(user.ID, req.Code) if err != nil { log.Debug().Err(err).Uint("user_id", user.ID).Msg("Email verification failed") return err } // Send post-verification welcome email with tips (async) if h.emailService != nil { go func() { if err := h.emailService.SendPostVerificationEmail(user.Email, user.FirstName); err != nil { log.Error().Err(err).Str("email", user.Email).Msg("Failed to send post-verification email") } }() } 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 echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } code, err := h.authService.ResendVerificationCode(user.ID) if err != nil { log.Debug().Err(err).Uint("user_id", user.ID).Msg("Failed to resend verification") return err } // 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") } }() } return c.JSON(http.StatusOK, responses.MessageResponse{Message: "Verification email sent"}) } // ForgotPassword handles POST /api/auth/forgot-password/ func (h *AuthHandler) ForgotPassword(c echo.Context) error { var req requests.ForgotPasswordRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } code, user, err := h.authService.ForgotPassword(req.Email) if err != nil { var appErr *apperrors.AppError if errors.As(err, &appErr) && appErr.Code == http.StatusTooManyRequests { // Only reveal rate limit errors return err } log.Error().Err(err).Str("email", req.Email).Msg("Forgot password failed") // Don't reveal other 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 return c.JSON(http.StatusOK, responses.ForgotPasswordResponse{ Message: "Password reset email sent", }) } // VerifyResetCode handles POST /api/auth/verify-reset-code/ func (h *AuthHandler) VerifyResetCode(c echo.Context) error { var req requests.VerifyResetCodeRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } resetToken, err := h.authService.VerifyResetCode(req.Email, req.Code) if err != nil { log.Debug().Err(err).Str("email", req.Email).Msg("Verify reset code failed") return err } return c.JSON(http.StatusOK, responses.VerifyResetCodeResponse{ Message: "Reset code verified", ResetToken: resetToken, }) } // ResetPassword handles POST /api/auth/reset-password/ func (h *AuthHandler) ResetPassword(c echo.Context) error { var req requests.ResetPasswordRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } err := h.authService.ResetPassword(req.ResetToken, req.NewPassword) if err != nil { log.Debug().Err(err).Msg("Password reset failed") return err } return c.JSON(http.StatusOK, responses.ResetPasswordResponse{ Message: "Password reset successful", }) } // AppleSignIn handles POST /api/auth/apple-sign-in/ func (h *AuthHandler) AppleSignIn(c echo.Context) error { var req requests.AppleSignInRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } if h.appleAuthService == nil { log.Error().Msg("Apple auth service not configured") return &apperrors.AppError{ Code: 500, MessageKey: "error.apple_signin_not_configured", } } response, err := h.authService.AppleSignIn(c.Request().Context(), h.appleAuthService, &req) if err != nil { // Check for legacy Apple Sign In error (not yet migrated) if errors.Is(err, services.ErrAppleSignInFailed) { log.Debug().Err(err).Msg("Apple Sign In failed (legacy error)") return apperrors.Unauthorized("error.invalid_apple_token") } log.Debug().Err(err).Msg("Apple Sign In failed") return err } // 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") } }() } return c.JSON(http.StatusOK, response) } // GoogleSignIn handles POST /api/auth/google-sign-in/ func (h *AuthHandler) GoogleSignIn(c echo.Context) error { var req requests.GoogleSignInRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } if h.googleAuthService == nil { log.Error().Msg("Google auth service not configured") return &apperrors.AppError{ Code: 500, MessageKey: "error.google_signin_not_configured", } } response, err := h.authService.GoogleSignIn(c.Request().Context(), h.googleAuthService, &req) if err != nil { // Check for legacy Google Sign In error (not yet migrated) if errors.Is(err, services.ErrGoogleSignInFailed) { log.Debug().Err(err).Msg("Google Sign In failed (legacy error)") return apperrors.Unauthorized("error.invalid_google_token") } log.Debug().Err(err).Msg("Google Sign In failed") return err } // Send welcome email for new users (async) if response.IsNewUser && h.emailService != nil && response.User.Email != "" { go func() { if err := h.emailService.SendGoogleWelcomeEmail(response.User.Email, response.User.FirstName); err != nil { log.Error().Err(err).Str("email", response.User.Email).Msg("Failed to send Google welcome email") } }() } return c.JSON(http.StatusOK, response) }