Coverage for src/lexigram/admin/auth/errors.py: 10%
62 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:31 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:31 +0800
1"""Admin authentication exceptions.
3Leaf exception hierarchy for admin auth. All exceptions are intentionally
4minimal — descriptive docstrings and a standard message only, no extra logic.
6``AdminAuthError`` extends ``DomainError`` because admin auth failures are
7expected, recoverable domain failures (invalid credentials, locked accounts,
8expired sessions) rather than infrastructure or programming errors.
9"""
11from __future__ import annotations
13from datetime import datetime
14from typing import Any
16from lexigram.contracts.exceptions.domain import DomainError
19class AdminAuthError(DomainError):
20 """Base exception for all admin authentication errors."""
22 _code: str = "LEX_ERR_ADMIN_010"
25class InvalidCredentialsError(AdminAuthError):
26 """Raised when email/password combination is incorrect."""
28 _code: str = "LEX_ERR_ADMIN_011"
31class AccountLockedError(AdminAuthError):
32 """Raised when an account is temporarily or permanently locked.
34 Args:
35 message: Human-readable description.
36 unlock_at: When the lock expires (None for permanent lockout).
37 retry_after: Seconds until retry is permitted.
38 reason: Categorised reason string (lockout, rate_limit, etc.).
39 """
41 _code: str = "LEX_ERR_ADMIN_012"
43 def __init__(
44 self,
45 message: str,
46 unlock_at: datetime | None = None,
47 retry_after: int | None = None,
48 reason: str = "lockout",
49 ) -> None:
50 super().__init__(message)
51 self.unlock_at = unlock_at
52 self.retry_after = retry_after
53 self.reason = reason
55 def to_payload(self) -> dict[str, Any]:
56 """Return a structured error payload for API responses.
58 Returns:
59 Dict with reason, unlock_at (ISO8601), and retry_after keys.
60 """
61 payload: dict[str, Any] = {"reason": self.reason}
62 if self.unlock_at is not None:
63 payload["unlock_at"] = self.unlock_at.isoformat()
64 if self.retry_after is not None:
65 payload["retry_after"] = self.retry_after
66 return payload
69class RateLimitExceededError(AdminAuthError):
70 """Raised when the IP-based rate limit is exceeded.
72 Args:
73 message: Human-readable description.
74 retry_after: Seconds until retry is permitted.
75 reason: Categorised reason string.
76 """
78 _code: str = "LEX_ERR_ADMIN_013"
80 def __init__(
81 self,
82 message: str,
83 retry_after: int | None = None,
84 reason: str = "rate_limit",
85 ) -> None:
86 super().__init__(message)
87 self.retry_after = retry_after
88 self.reason = reason
90 def to_payload(self) -> dict[str, Any]:
91 """Return a structured error payload for API responses.
93 Returns:
94 Dict with reason and retry_after keys.
95 """
96 payload: dict[str, Any] = {"reason": self.reason}
97 if self.retry_after is not None:
98 payload["retry_after"] = self.retry_after
99 return payload
102class SessionNotFoundError(AdminAuthError):
103 """Raised when the requested session does not exist."""
105 _code: str = "LEX_ERR_ADMIN_014"
108class SessionExpiredError(AdminAuthError):
109 """Raised when the session has exceeded its idle or absolute timeout."""
111 _code: str = "LEX_ERR_ADMIN_015"
114class CsrfValidationError(AdminAuthError):
115 """Raised when CSRF token is missing, invalid, or expired."""
117 _code: str = "LEX_ERR_ADMIN_016"
120class PasswordPolicyError(AdminAuthError):
121 """Raised when a password does not meet policy requirements."""
123 _code: str = "LEX_ERR_ADMIN_017"
126class SetupAlreadyCompletedError(AdminAuthError):
127 """Raised when setup is attempted after an admin account already exists."""
129 _code: str = "LEX_ERR_ADMIN_018"
132class SetupTokenInvalidError(AdminAuthError):
133 """Raised when the ADMIN_SETUP_TOKEN env var is set and the provided token doesn't match."""
135 _code: str = "LEX_ERR_ADMIN_019"
138class PasswordResetTokenInvalidError(AdminAuthError):
139 """Raised when a password reset token is unknown or already consumed."""
141 _code: str = "LEX_ERR_ADMIN_020"
144class PasswordResetTokenExpiredError(AdminAuthError):
145 """Raised when a password reset token has expired."""
147 _code: str = "LEX_ERR_ADMIN_021"
150class MfaNotEnabledError(AdminAuthError):
151 """Raised when 2FA is required but not configured/enabled."""
153 _code: str = "LEX_ERR_ADMIN_022"
156class MfaVerificationFailedError(AdminAuthError):
157 """Raised when a TOTP code is missing, invalid, or expired."""
159 _code: str = "LEX_ERR_ADMIN_023"
162class EmailVerificationRequiredError(AdminAuthError):
163 """Raised when login is blocked because the email is unverified."""
165 _code: str = "LEX_ERR_ADMIN_024"
168class EmailVerificationTokenInvalidError(AdminAuthError):
169 """Raised when a verification token is missing, invalid, used, or expired."""
171 _code: str = "LEX_ERR_ADMIN_025"
174class EmailOtpDeliveryError(AdminAuthError):
175 """Raised when an email OTP cannot be delivered."""
177 _code: str = "LEX_ERR_ADMIN_026"
180class EmailOtpCooldownError(AdminAuthError):
181 """Raised when an email OTP resend is attempted too soon."""
183 _code: str = "LEX_ERR_ADMIN_027"
186__all__ = [
187 "AccountLockedError",
188 "AdminAuthError",
189 "CsrfValidationError",
190 "EmailOtpCooldownError",
191 "EmailOtpDeliveryError",
192 "EmailVerificationRequiredError",
193 "EmailVerificationTokenInvalidError",
194 "InvalidCredentialsError",
195 "MfaNotEnabledError",
196 "MfaVerificationFailedError",
197 "PasswordPolicyError",
198 "PasswordResetTokenExpiredError",
199 "PasswordResetTokenInvalidError",
200 "RateLimitExceededError",
201 "SessionExpiredError",
202 "SessionNotFoundError",
203 "SetupAlreadyCompletedError",
204 "SetupTokenInvalidError",
205]