198 lines
6.6 KiB
Python
198 lines
6.6 KiB
Python
"""User repository interface for abstracting database operations."""
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
from app.gateway.auth.models import User
|
|
|
|
|
|
class UserNotFoundError(LookupError):
|
|
"""Raised when a user repository operation targets a non-existent row.
|
|
|
|
Subclass of :class:`LookupError` so callers that already catch
|
|
``LookupError`` for "missing entity" can keep working unchanged,
|
|
while specific call sites can pin to this class to distinguish
|
|
"concurrent delete during update" from other lookups.
|
|
"""
|
|
|
|
|
|
class CredentialConflictError(ValueError):
|
|
"""Credentials changed after the caller authenticated the old password."""
|
|
|
|
|
|
class UserRepository(ABC):
|
|
"""Abstract interface for user data storage.
|
|
|
|
Implement this interface to support different storage backends
|
|
(SQLite)
|
|
"""
|
|
|
|
@abstractmethod
|
|
async def create_user(self, user: User) -> User:
|
|
"""Create a new user.
|
|
|
|
Args:
|
|
user: User object to create
|
|
|
|
Returns:
|
|
Created User with ID assigned
|
|
|
|
Raises:
|
|
ValueError: If email already exists
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def get_user_by_id(self, user_id: str) -> User | None:
|
|
"""Get user by ID.
|
|
|
|
Args:
|
|
user_id: User UUID as string
|
|
|
|
Returns:
|
|
User if found, None otherwise
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def get_user_by_email(self, email: str) -> User | None:
|
|
"""Get user by email.
|
|
|
|
Args:
|
|
email: User email address
|
|
|
|
Returns:
|
|
User if found, None otherwise
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
async def get_users_by_ids(self, user_ids: list[str]) -> list[User]:
|
|
"""Batch lookup users; fallback keeps lightweight repositories compatible."""
|
|
users = []
|
|
for user_id in dict.fromkeys(user_ids):
|
|
user = await self.get_user_by_id(user_id)
|
|
if user is not None:
|
|
users.append(user)
|
|
return users
|
|
|
|
@abstractmethod
|
|
async def find_user_by_email_local_part(self, local_part: str) -> User | None:
|
|
"""Find a user by the local part of their email (the bit before ``@``).
|
|
|
|
Used by the passwordless username-login flow so that a single
|
|
username keeps mapping to the same account even when the synthetic
|
|
email domain (e.g. ``cm.com`` vs the legacy ``local.deerflow``)
|
|
differs between registration and current login.
|
|
|
|
Returns the oldest matching user when multiple rows share the same
|
|
local part, so collisions resolve deterministically.
|
|
|
|
Args:
|
|
local_part: Substring to match before the ``@``. Case-insensitive.
|
|
|
|
Returns:
|
|
User if found, None otherwise.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
async def find_users_by_email_local_part_like(self, substring: str, *, limit: int = 500) -> list["User"]:
|
|
"""Fuzzy-match users whose email local part *contains* ``substring``.
|
|
|
|
Backs the admin LLM-metrics username filter (按用户名模糊搜索). Unlike
|
|
:meth:`find_user_by_email_local_part` (exact prefix → single account),
|
|
this returns every matching account. Case-insensitive substring match
|
|
against the part before ``@``. Non-overriding repositories return an
|
|
empty list.
|
|
|
|
Args:
|
|
substring: Case-insensitive substring to match within the local part.
|
|
limit: Maximum number of users to return.
|
|
|
|
Returns:
|
|
Matching users (may be empty).
|
|
"""
|
|
return []
|
|
|
|
async def update_credentials(self, user: User, password_hash: str | None, *, email: str | None = None) -> User:
|
|
"""Fallback for non-SQL implementations; SQL overrides with atomic CAS."""
|
|
user.password_hash = password_hash
|
|
user.login_password = password_hash
|
|
user.token_version += 1
|
|
user.needs_setup = False
|
|
if email is not None:
|
|
user.email = email
|
|
return await self.update_user(user)
|
|
|
|
async def create_initial_admin(self, user: User) -> User:
|
|
if await self.count_admin_users():
|
|
raise ValueError("System already initialized")
|
|
return await self.create_user(user)
|
|
|
|
async def start_login_session(self, user: User) -> User:
|
|
"""Invalidate earlier sessions after a successful credential check."""
|
|
user.token_version += 1
|
|
return await self.update_user(user)
|
|
|
|
@abstractmethod
|
|
async def update_user(self, user: User) -> User:
|
|
"""Update an existing user.
|
|
|
|
Args:
|
|
user: User object with updated fields
|
|
|
|
Returns:
|
|
Updated User
|
|
|
|
Raises:
|
|
UserNotFoundError: If no row exists for ``user.id``. This is
|
|
a hard failure (not a no-op) so callers cannot mistake a
|
|
concurrent-delete race for a successful update.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def count_users(self) -> int:
|
|
"""Return total number of registered users."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def count_admin_users(self) -> int:
|
|
"""Return number of users with system_role == 'admin'."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def list_users(self) -> list[User]:
|
|
"""Return all users ordered by creation date."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def list_users_page(
|
|
self, *, q: str | None = None, limit: int = 20, offset: int = 0
|
|
) -> list[User]:
|
|
"""Return one page of users ordered by creation date.
|
|
|
|
``q`` optionally matches (case-insensitive substring) email or user id.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def count_users_filtered(
|
|
self, *, q: str | None = None, approved_or_admin: bool = False
|
|
) -> int:
|
|
"""Count users, optionally filtered by the same ``q`` as
|
|
:meth:`list_users_page`; ``approved_or_admin`` restricts to users the
|
|
login whitelist would let in (approved or admin role)."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def get_user_by_oauth(self, provider: str, oauth_id: str) -> User | None:
|
|
"""Get user by OAuth provider and ID.
|
|
|
|
Args:
|
|
provider: OAuth provider name (e.g. 'github', 'google')
|
|
oauth_id: User ID from the OAuth provider
|
|
|
|
Returns:
|
|
User if found, None otherwise
|
|
"""
|
|
raise NotImplementedError
|