Coverage for src / lexigram / admin / auth / errors.py: 63%
46 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +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"
138__all__ = [
139 "AccountLockedError",
140 "AdminAuthError",
141 "CsrfValidationError",
142 "InvalidCredentialsError",
143 "PasswordPolicyError",
144 "RateLimitExceededError",
145 "SessionExpiredError",
146 "SessionNotFoundError",
147 "SetupAlreadyCompletedError",
148 "SetupTokenInvalidError",
149]