Coverage for src/lexigram/admin/auth/store/protocols.py: 33%
18 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 user store protocol.
3Defines the formal contract for admin panel user store implementations.
4Any class that satisfies these method signatures is a valid implementation.
5"""
7from __future__ import annotations
9from typing import Any, Protocol, runtime_checkable
11from lexigram.admin.auth.errors import SetupAlreadyCompletedError
12from lexigram.result import Result
15@runtime_checkable
16class AdminUserStoreProtocol(Protocol):
17 """Protocol for admin panel user store operations.
19 This is the single authoritative contract for anything that stores and
20 manages admin-panel user accounts (distinct from the application's own
21 user store managed by lexigram-auth).
23 Implementations:
24 - :class:`~lexigram.admin.auth.store.direct_sql.DirectSQLAdminUserStore`
25 — production SQL backend (``admin_users`` table)
26 - :class:`~lexigram.admin.auth.store.memory.MemoryAdminUserStore`
27 — in-memory store for testing
28 """
30 async def get_admin_count(self) -> int:
31 """Return the total number of admin-panel accounts.
33 Used by :class:`~lexigram.admin.middleware.setup.SetupMiddleware` to
34 decide whether to redirect to the first-run setup wizard.
36 Returns:
37 Non-negative integer count of admin users.
38 """
39 ...
41 async def ensure_schema(self) -> None:
42 """Create the admin_users table if it does not exist (idempotent).
44 Failures are logged and swallowed, never raised — matching the boot
45 loop's swallow-and-log behavior.
46 """
47 ...
49 async def list_users(self) -> list[Any]:
50 """Return all admin users ordered by creation time."""
51 ...
53 async def create_user(
54 self,
55 name: str,
56 email: str,
57 hashed_password: str,
58 roles: list[str] | None = None,
59 permissions: list[str] | None = None,
60 **kwargs: Any,
61 ) -> Any:
62 """Create (or upsert) an admin-panel user account.
64 Args:
65 name: Display name.
66 email: Unique email address — used as the login identifier.
67 hashed_password: Pre-hashed credential.
68 roles: Optional list of role strings (e.g. ``["superadmin"]``).
69 permissions: Optional list of explicit permission strings.
70 **kwargs: Implementation-specific extras (ignored if unsupported).
72 Returns:
73 A lightweight object exposing at least ``user_id``, ``name``, and
74 ``email`` attributes.
75 """
76 ...
78 async def claim_first_admin(
79 self,
80 name: str,
81 email: str,
82 hashed_password: str,
83 roles: list[str],
84 ) -> Result[Any, SetupAlreadyCompletedError]:
85 """Atomically create the first admin account if none exists yet.
87 Implementations must make the emptiness-check plus insert a single
88 atomic operation (e.g. ``INSERT ... SELECT ... WHERE NOT EXISTS``
89 or an ``asyncio.Lock`` around an in-memory insert), so that two
90 concurrent ``POST /setup`` submissions cannot both succeed.
92 Args:
93 name: Display name.
94 email: Unique email address — used as the login identifier.
95 hashed_password: Pre-hashed credential.
96 roles: Role strings for the new account (e.g. ``["superadmin"]``).
98 Returns:
99 Ok(user-like object) when this call created the first admin
100 account; ``Err(SetupAlreadyCompletedError)`` when at least one
101 admin account already exists and nothing was inserted.
102 """
103 ...
105 async def get_user_by_email(self, email: str) -> Any | None:
106 """Look up an admin user by email address.
108 Args:
109 email: Email to search for.
111 Returns:
112 User object or ``None`` when no match exists.
113 """
114 ...
116 async def get_user_by_id(self, user_id: str) -> Any | None:
117 """Look up an admin user by primary key.
119 Args:
120 user_id: Unique identifier (UUID string).
122 Returns:
123 User object or ``None`` when no match exists.
124 """
125 ...
127 async def get_by_id(self, user_id: str) -> Any | None:
128 """Look up the authenticated user backing a panel session.
130 Called by :class:`~lexigram.admin.middleware.auth.AdminAuthMiddleware`
131 while loading the user stored in ``admin_sessions``; the returned
132 object must expose ``user_id`` and ``is_active`` for that path.
134 Args:
135 user_id: Unique identifier (UUID string).
137 Returns:
138 User object or ``None`` when no match exists.
139 """
140 ...
142 async def update_user(self, user: Any) -> None:
143 """Persist changes to an existing admin user.
145 Args:
146 user: User object carrying updated field values. Must expose at
147 least ``user_id``, ``name``, ``email``, ``roles``,
148 ``permissions``, ``hashed_password``, and ``is_active``.
149 """
150 ...
152 async def delete_user(self, user_id: str) -> None:
153 """Permanently remove an admin user account.
155 Args:
156 user_id: Unique identifier of the user to delete.
157 """
158 ...
160 async def authenticate(self, email: str, password: str) -> Any | None:
161 """Authenticate an admin user by email and password.
163 Args:
164 email: Email address to look up.
165 password: Plain-text password to verify.
167 Returns:
168 User object when credentials are valid and account is active,
169 ``None`` otherwise.
170 """
171 ...
174__all__ = ["AdminUserStoreProtocol"]