AIDA / app /core /exceptions.py
destinyebuka's picture
dora
4c9881b
# ============================================================
# 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},
)