# ============================================================ # app/core/exceptions.py - ALL Auth Exceptions # ============================================================ from fastapi import HTTPException, status from typing import Optional, Any, Dict class AuthException(HTTPException): """Base authentication exception""" def __init__( self, status_code: int, detail: str, error_code: str, message: str, data: Optional[Dict[str, Any]] = None, ): super().__init__(status_code=status_code, detail=detail) self.error_code = error_code self.message = message self.data = data or {} # ============================================================ # LOGIN ERRORS # ============================================================ class InvalidCredentialsException(AuthException): """Invalid email/phone or password""" def __init__(self): super().__init__( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", error_code="INVALID_CREDENTIALS", message="The email/phone or password you entered is incorrect. Please try again.", ) class AccountInactiveException(AuthException): """Account is inactive""" def __init__(self): super().__init__( status_code=status.HTTP_403_FORBIDDEN, detail="Account is inactive", error_code="ACCOUNT_INACTIVE", message="Your account has been deactivated. Please contact support for assistance.", ) # ============================================================ # SIGNUP ERRORS # ============================================================ class UserAlreadyExistsException(AuthException): """User already registered""" def __init__(self, identifier: str): super().__init__( status_code=status.HTTP_409_CONFLICT, detail="User already registered", error_code="USER_ALREADY_EXISTS", message="This email or phone number is already registered. Please log in instead.", data={"identifier": identifier}, ) class SignupFailedException(AuthException): """Signup failed due to server error""" def __init__(self, reason: str = "Unknown error"): super().__init__( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Signup failed", error_code="SIGNUP_FAILED", message=f"Unable to create account. {reason} Please try again later.", ) class InvalidEmailFormatException(AuthException): """Invalid email format""" def __init__(self): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid email format", error_code="INVALID_EMAIL_FORMAT", message="Please enter a valid email address.", ) class InvalidPhoneFormatException(AuthException): """Invalid phone format""" def __init__(self): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid phone format", error_code="INVALID_PHONE_FORMAT", message="Please enter a valid phone number.", ) class WeakPasswordException(AuthException): """Password doesn't meet requirements""" def __init__(self, requirement: str): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="Password doesn't meet requirements", error_code="WEAK_PASSWORD", message=f"Password must {requirement}. Please choose a stronger password.", ) class MissingEmailPhoneException(AuthException): """Neither email nor phone provided""" def __init__(self): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="Email or phone required", error_code="MISSING_EMAIL_PHONE", message="Please provide either an email address or a phone number.", ) # ============================================================ # OTP ERRORS # ============================================================ class InvalidOtpException(AuthException): """Invalid OTP code""" def __init__(self, attempts_left: Optional[int] = None): msg = "The OTP code you entered is incorrect." if attempts_left is not None: msg += f" You have {attempts_left} attempt(s) left." super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid OTP code", error_code="INVALID_OTP", message=msg, data={"attempts_left": attempts_left} if attempts_left else {}, ) class OtpExpiredException(AuthException): """OTP has expired""" def __init__(self): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="OTP expired", error_code="OTP_EXPIRED", message="The OTP code has expired. Please request a new one.", ) class OtpNotSentException(AuthException): """OTP was never sent""" def __init__(self): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="OTP not found", error_code="OTP_NOT_FOUND", message="No OTP was sent. Please request one first.", ) class OtpSendFailedException(AuthException): """Failed to send OTP""" def __init__(self, method: str = "email/SMS"): super().__init__( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="OTP send failed", error_code="OTP_SEND_FAILED", message=f"Failed to send OTP via {method}. Please try again later.", ) class OtpAlreadyValidException(AuthException): """OTP already validated/used""" def __init__(self): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="OTP already used", error_code="OTP_ALREADY_USED", message="This OTP has already been used. Please request a new one.", ) class OtpTooManyAttemptsException(AuthException): """Too many failed OTP attempts""" def __init__(self, retry_after_minutes: int = 15): super().__init__( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many OTP attempts", error_code="OTP_TOO_MANY_ATTEMPTS", message=f"Too many failed attempts. Please try again in {retry_after_minutes} minutes.", data={"retry_after_minutes": retry_after_minutes}, ) # ============================================================ # PASSWORD RESET ERRORS # ============================================================ class InvalidResetTokenException(AuthException): """Invalid reset token""" def __init__(self): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid reset token", error_code="INVALID_RESET_TOKEN", message="The reset token is invalid or has expired. Please request a password reset again.", ) class ResetTokenExpiredException(AuthException): """Reset token has expired""" def __init__(self): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="Reset token expired", error_code="RESET_TOKEN_EXPIRED", message="The reset token has expired (valid for 10 minutes). Please request a new password reset.", ) class TokenMismatchException(AuthException): """Token identifier doesn't match request identifier""" def __init__(self): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="Token mismatch", error_code="TOKEN_MISMATCH", message="The reset token doesn't match. Please request a new password reset.", ) class PasswordResetFailedException(AuthException): """Password reset failed""" def __init__(self): super().__init__( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Password reset failed", error_code="PASSWORD_RESET_FAILED", message="Unable to reset your password. Please try again later.", ) # ============================================================ # USER ERRORS # ============================================================ class UserNotFoundException(AuthException): """User not found""" def __init__(self, identifier: str): super().__init__( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", error_code="USER_NOT_FOUND", message="No account found with this email or phone number. Please sign up first.", data={"identifier": identifier}, ) class AccountNotVerifiedException(AuthException): """Account email/phone not verified""" def __init__(self, verification_type: str = "email"): super().__init__( status_code=status.HTTP_403_FORBIDDEN, detail="Account not verified", error_code="ACCOUNT_NOT_VERIFIED", message=f"Your {verification_type} has not been verified yet. Please verify to continue.", data={"verification_type": verification_type}, ) class SamePasswordException(AuthException): """New password is same as old password""" def __init__(self): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="Same password", error_code="SAME_PASSWORD", message="Your new password cannot be the same as your current password. Please choose a different one.", ) # ============================================================ # DATABASE ERRORS # ============================================================ class DatabaseException(AuthException): """Database operation failed""" def __init__(self, operation: str = "operation"): super().__init__( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database error", error_code="DATABASE_ERROR", message=f"A database error occurred during {operation}. Please try again later.", ) # ============================================================ # VALIDATION ERRORS # ============================================================ class ValidationException(AuthException): """Validation error""" def __init__(self, message: str, errors: Optional[list] = None): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="Validation error", error_code="VALIDATION_ERROR", message=message, data={"errors": errors} if errors else {}, ) # ============================================================ # RATE LIMITING ERRORS # ============================================================ class RateLimitException(AuthException): """Too many requests""" def __init__(self, retry_after_seconds: int = 60): super().__init__( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many requests", error_code="RATE_LIMIT_EXCEEDED", message=f"You've made too many requests. Please try again in {retry_after_seconds} seconds.", data={"retry_after_seconds": retry_after_seconds}, ) # ============================================================ # RESEND OTP ERRORS # ============================================================ class OtpResendTooSoonException(AuthException): """OTP resend requested too soon""" def __init__(self, wait_seconds: int = 60): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="OTP resend too soon", error_code="OTP_RESEND_TOO_SOON", message=f"Please wait {wait_seconds} seconds before requesting a new OTP.", data={"wait_seconds": wait_seconds}, ) class OtpStillValidException(AuthException): """OTP is still valid, don't resend yet""" def __init__(self, expires_in_seconds: int): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, detail="OTP still valid", error_code="OTP_STILL_VALID", message=f"Your current OTP is still valid. Please use it first (expires in {expires_in_seconds} seconds).", data={"expires_in_seconds": expires_in_seconds}, )