package handler import ( "encoding/json" "net/http" "github.com/johnzastrow/actalog/internal/service" "github.com/johnzastrow/actalog/pkg/logger" ) // AuthHandler handles authentication endpoints type AuthHandler struct { userService *service.UserService logger *logger.Logger } // NewAuthHandler creates a new auth handler func NewAuthHandler(userService *service.UserService, l *logger.Logger) *AuthHandler { return &AuthHandler{ userService: userService, logger: l, } } // RegisterRequest represents a registration request // @Description User registration request body type RegisterRequest struct { Name string `json:"name" example:"John Doe"` Email string `json:"email" example:"john@example.com"` Password string `json:"password" example:"securePassword123"` } // LoginRequest represents a login request // @Description User login request body type LoginRequest struct { Email string `json:"email" example:"john@example.com"` Password string `json:"password" example:"securePassword123"` RememberMe bool `json:"remember_me,omitempty" example:"true"` } // AuthResponse represents an authentication response // @Description Authentication response containing JWT token and user info type AuthResponse struct { Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` RefreshToken string `json:"refresh_token,omitempty" example:"550e8400-e29b-41d4-a716-446655440000"` User interface{} `json:"user"` } // Register handles user registration // @Summary Register a new user // @Description Create a new user account with name, email, and password // @Tags auth // @Accept json // @Produce json // @Param request body RegisterRequest true "Registration details" // @Success 201 {object} AuthResponse "Successfully registered" // @Failure 400 {object} ErrorResponse "Invalid request or validation error" // @Failure 403 {object} ErrorResponse "Registration is closed" // @Failure 409 {object} ErrorResponse "Email already exists" // @Router /auth/register [post] func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { var req RegisterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondErrorWithDetail(w, http.StatusBadRequest, "Invalid request body", err.Error()) return } // Validate input if req.Name == "" || req.Email == "" || req.Password == "" { respondError(w, http.StatusBadRequest, "Name, email, and password are required") return } // Log attempt if h.logger != nil { h.logger.Info("action=register_attempt email=%s name=%s remote=%s ua=%s", req.Email, req.Name, r.RemoteAddr, r.UserAgent()) } // Register user user, token, err := h.userService.Register(req.Name, req.Email, req.Password) if err != nil { switch err { case service.ErrEmailAlreadyExists: if h.logger != nil { h.logger.Warn("action=register outcome=failure email=%s reason=email_exists", req.Email) } respondError(w, http.StatusConflict, "Email already exists") case service.ErrRegistrationClosed: if h.logger != nil { h.logger.Warn("action=register outcome=failure email=%s reason=registration_closed", req.Email) } respondError(w, http.StatusForbidden, "Registration is closed. Please contact an administrator.") default: if h.logger != nil { h.logger.Error("action=register outcome=failure email=%s error=%v", req.Email, err) } // Return the actual validation error to help users fix their input respondError(w, http.StatusBadRequest, err.Error()) } return } if h.logger != nil { h.logger.Info("action=register outcome=success user_id=%d email=%s", user.ID, user.Email) } respondJSON(w, http.StatusCreated, AuthResponse{ Token: token, User: user, }) } // Login handles user login // @Summary User login // @Description Authenticate user with email and password. Set remember_me to true for refresh token. // @Tags auth // @Accept json // @Produce json // @Param request body LoginRequest true "Login credentials" // @Success 200 {object} AuthResponse "Successfully authenticated" // @Failure 400 {object} ErrorResponse "Invalid request body" // @Failure 401 {object} ErrorResponse "Invalid email or password" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /auth/login [post] func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondErrorWithDetail(w, http.StatusBadRequest, "Invalid request body", err.Error()) return } // Validate input if req.Email == "" || req.Password == "" { respondError(w, http.StatusBadRequest, "Email and password are required") return } if h.logger != nil { h.logger.Info("action=login_attempt email=%s remote=%s ua=%s", req.Email, r.RemoteAddr, r.UserAgent()) } // Login user user, token, err := h.userService.Login(req.Email, req.Password) if err != nil { if err == service.ErrInvalidCredentials { if h.logger != nil { h.logger.Warn("action=login outcome=failure email=%s reason=invalid_credentials", req.Email) } respondError(w, http.StatusUnauthorized, "Invalid email or password") } else { if h.logger != nil { h.logger.Error("action=login outcome=failure email=%s error=%v", req.Email, err) } respondErrorWithDetail(w, http.StatusInternalServerError, "Failed to login", err.Error()) } return } response := AuthResponse{ Token: token, User: user, } // Create refresh token if remember_me is true if req.RememberMe { deviceInfo := r.UserAgent() // Get browser/device info from User-Agent header refreshToken, err := h.userService.CreateRefreshToken(user.ID, deviceInfo, req.RememberMe) if err != nil { // Log error but don't fail the login if h.logger != nil { h.logger.Warn("action=create_refresh_token outcome=failure user_id=%d email=%s error=%v", user.ID, user.Email, err) } // User can still use the access token respondErrorWithDetail(w, http.StatusInternalServerError, "Warning: Failed to create refresh token", err.Error()) } else { response.RefreshToken = refreshToken if h.logger != nil { h.logger.Info("action=create_refresh_token outcome=success user_id=%d email=%s remember_me=%v", user.ID, user.Email, req.RememberMe) } } } if h.logger != nil { h.logger.Info("action=login outcome=success user_id=%d email=%s", user.ID, user.Email) } respondJSON(w, http.StatusOK, response) } // ForgotPasswordRequest represents a forgot password request // @Description Request to initiate password reset type ForgotPasswordRequest struct { Email string `json:"email" example:"john@example.com"` } // ResetPasswordRequest represents a reset password request // @Description Request to complete password reset with token type ResetPasswordRequest struct { Token string `json:"token" example:"abc123def456"` NewPassword string `json:"new_password" example:"newSecurePassword123"` } // MessageResponse represents a simple message response // @Description Generic message response type MessageResponse struct { Message string `json:"message" example:"Operation completed successfully"` } // ForgotPassword handles forgot password requests // @Summary Request password reset // @Description Send a password reset email to the user. Always returns success for security. // @Tags auth // @Accept json // @Produce json // @Param request body ForgotPasswordRequest true "Email address" // @Success 200 {object} MessageResponse "Reset email sent if address exists" // @Failure 400 {object} ErrorResponse "Invalid request body" // @Failure 500 {object} ErrorResponse "Failed to process request" // @Router /auth/forgot-password [post] func (h *AuthHandler) ForgotPassword(w http.ResponseWriter, r *http.Request) { var req ForgotPasswordRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Validate email if req.Email == "" { respondError(w, http.StatusBadRequest, "Email is required") return } // Request password reset (always succeeds for security) err := h.userService.RequestPasswordReset(req.Email) if err != nil { // Log error but don't reveal to user // In production, this should use proper logging respondError(w, http.StatusInternalServerError, "Failed to process request") return } // Always return success (don't reveal if email exists) respondJSON(w, http.StatusOK, MessageResponse{ Message: "If your email is registered, you will receive a password reset link shortly", }) } // ResetPassword handles password reset requests // @Summary Reset password // @Description Complete password reset using token from email // @Tags auth // @Accept json // @Produce json // @Param request body ResetPasswordRequest true "Reset token and new password" // @Success 200 {object} MessageResponse "Password reset successfully" // @Failure 400 {object} ErrorResponse "Invalid token, expired token, or weak password" // @Failure 500 {object} ErrorResponse "Failed to reset password" // @Router /auth/reset-password [post] func (h *AuthHandler) ResetPassword(w http.ResponseWriter, r *http.Request) { var req ResetPasswordRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Validate input if req.Token == "" || req.NewPassword == "" { respondError(w, http.StatusBadRequest, "Token and new password are required") return } // Reset password (policy enforced in service layer) err := h.userService.ResetPassword(req.Token, req.NewPassword) if err != nil { switch err { case service.ErrInvalidResetToken: respondError(w, http.StatusBadRequest, "Invalid reset token") case service.ErrResetTokenExpired: respondError(w, http.StatusBadRequest, "Reset token has expired. Please request a new one") default: respondError(w, http.StatusInternalServerError, "Failed to reset password") } return } respondJSON(w, http.StatusOK, MessageResponse{ Message: "Password has been reset successfully. You can now login with your new password", }) } // VerifyEmailRequest represents an email verification request // @Description Email verification request type VerifyEmailRequest struct { Token string `json:"token" example:"verification-token-123"` } // VerifyEmail handles email verification requests // @Summary Verify email address // @Description Verify user's email using token from verification email // @Tags auth // @Accept json // @Produce json // @Param token query string true "Verification token from email" // @Success 200 {object} MessageResponse "Email verified successfully" // @Failure 400 {object} ErrorResponse "Invalid or expired token, or already verified" // @Failure 500 {object} ErrorResponse "Failed to verify email" // @Router /auth/verify-email [get] func (h *AuthHandler) VerifyEmail(w http.ResponseWriter, r *http.Request) { // Get token from query parameter token := r.URL.Query().Get("token") if token == "" { respondError(w, http.StatusBadRequest, "Verification token is required") return } // Verify email err := h.userService.VerifyEmail(token) if err != nil { switch err { case service.ErrInvalidVerificationToken: respondError(w, http.StatusBadRequest, "Invalid verification token") case service.ErrVerificationTokenExpired: respondError(w, http.StatusBadRequest, "Verification token has expired. Please request a new one") case service.ErrEmailAlreadyVerified: respondError(w, http.StatusBadRequest, "Email is already verified") default: respondError(w, http.StatusInternalServerError, "Failed to verify email") } return } respondJSON(w, http.StatusOK, MessageResponse{ Message: "Email verified successfully. You can now login", }) } // ResendVerificationRequest represents a resend verification email request // @Description Request to resend verification email type ResendVerificationRequest struct { Email string `json:"email" example:"john@example.com"` } // ResendVerification handles resend verification email requests // @Summary Resend verification email // @Description Resend email verification link. Always returns success for security. // @Tags auth // @Accept json // @Produce json // @Param request body ResendVerificationRequest true "Email address" // @Success 200 {object} MessageResponse "Verification email sent if applicable" // @Failure 400 {object} ErrorResponse "Invalid request or already verified" // @Failure 500 {object} ErrorResponse "Failed to resend verification email" // @Router /auth/resend-verification [post] func (h *AuthHandler) ResendVerification(w http.ResponseWriter, r *http.Request) { var req ResendVerificationRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Validate email if req.Email == "" { respondError(w, http.StatusBadRequest, "Email is required") return } // Resend verification email err := h.userService.ResendVerificationEmail(req.Email) if err != nil { if err == service.ErrEmailAlreadyVerified { respondError(w, http.StatusBadRequest, "Email is already verified") return } respondError(w, http.StatusInternalServerError, "Failed to resend verification email") return } // Always return success (don't reveal if email exists) respondJSON(w, http.StatusOK, MessageResponse{ Message: "If your email is registered and not yet verified, you will receive a verification link shortly", }) } // RefreshTokenRequest represents a refresh token request // @Description Request to refresh access token type RefreshTokenRequest struct { RefreshToken string `json:"refresh_token" example:"550e8400-e29b-41d4-a716-446655440000"` } // RefreshToken handles refresh token requests // @Summary Refresh access token // @Description Get a new access token using a valid refresh token // @Tags auth // @Accept json // @Produce json // @Param request body RefreshTokenRequest true "Refresh token" // @Success 200 {object} AuthResponse "New access token" // @Failure 400 {object} ErrorResponse "Invalid request body" // @Failure 401 {object} ErrorResponse "Invalid or expired refresh token" // @Failure 500 {object} ErrorResponse "Failed to refresh token" // @Router /auth/refresh-token [post] func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { var req RefreshTokenRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Validate input if req.RefreshToken == "" { respondError(w, http.StatusBadRequest, "Refresh token is required") return } // Refresh access token user, newAccessToken, err := h.userService.RefreshAccessToken(req.RefreshToken) if err != nil { if err == service.ErrInvalidRefreshToken { respondError(w, http.StatusUnauthorized, "Invalid or expired refresh token") } else { respondError(w, http.StatusInternalServerError, "Failed to refresh token") } return } respondJSON(w, http.StatusOK, AuthResponse{ Token: newAccessToken, User: user, }) } // RevokeTokenRequest represents a revoke token request // @Description Request to revoke a refresh token (logout) type RevokeTokenRequest struct { RefreshToken string `json:"refresh_token" example:"550e8400-e29b-41d4-a716-446655440000"` } // RevokeToken handles token revocation (logout) // @Summary Revoke refresh token // @Description Invalidate a refresh token to logout from a specific session // @Tags auth // @Accept json // @Produce json // @Param request body RevokeTokenRequest true "Refresh token to revoke" // @Success 200 {object} MessageResponse "Token revoked successfully" // @Failure 400 {object} ErrorResponse "Invalid request body" // @Failure 404 {object} ErrorResponse "Refresh token not found" // @Failure 500 {object} ErrorResponse "Failed to revoke token" // @Router /auth/revoke-token [post] func (h *AuthHandler) RevokeToken(w http.ResponseWriter, r *http.Request) { var req RevokeTokenRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Validate input if req.RefreshToken == "" { respondError(w, http.StatusBadRequest, "Refresh token is required") return } // Revoke token err := h.userService.RevokeRefreshToken(req.RefreshToken) if err != nil { if err == service.ErrInvalidRefreshToken { respondError(w, http.StatusNotFound, "Refresh token not found") } else { respondError(w, http.StatusInternalServerError, "Failed to revoke token") } return } respondJSON(w, http.StatusOK, MessageResponse{ Message: "Token revoked successfully", }) } // Helper functions func respondJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } func respondError(w http.ResponseWriter, status int, message string) { respondJSON(w, status, ErrorResponse{Message: message}) } func respondErrorWithDetail(w http.ResponseWriter, status int, message string, detail string) { respondJSON(w, status, ErrorResponse{Message: message, Error: detail}) }