Spaces:
Running
Running
| # ============================================================ | |
| # 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}, | |
| ) |