"""Local email/password authentication provider.""" import logging from app.gateway.auth.models import User from app.gateway.auth.password import hash_password_async, needs_rehash, verify_password_async from app.gateway.auth.providers import AuthProvider from app.gateway.auth.repositories.base import UserRepository logger = logging.getLogger(__name__) class LocalAuthProvider(AuthProvider): """Email/password authentication provider using local database.""" def __init__(self, repository: UserRepository): """Initialize with a UserRepository. Args: repository: UserRepository implementation (SQLite) """ self._repo = repository async def authenticate(self, credentials: dict) -> User | None: """Authenticate with email and password. Args: credentials: dict with 'email' and 'password' keys Returns: User if authentication succeeds, None otherwise """ email = credentials.get("email") password = credentials.get("password") if not email or not password: return None user = await self._repo.get_user_by_email(email) if user is None: return None if user.password_hash is None: # OAuth user without local password return None if not await verify_password_async(password, user.password_hash): return None if needs_rehash(user.password_hash): try: user.password_hash = await hash_password_async(password) await self._repo.update_user(user) except Exception: # Rehash is an opportunistic upgrade; a transient DB error must not # prevent an otherwise-valid login from succeeding. logger.warning("Failed to rehash password for user %s; login will still succeed", user.email, exc_info=True) return user async def get_user(self, user_id: str) -> User | None: """Get user by ID.""" return await self._repo.get_user_by_id(user_id) async def find_user_by_email_local_part(self, local_part: str) -> User | None: """Find a user whose email starts with ``local_part@`` regardless of domain. Used by the passwordless username-login flow to keep a single username mapped to the same account across legacy / canonical email-domain defaults. """ return await self._repo.find_user_by_email_local_part(local_part) async def find_users_by_email_local_part_like(self, substring: str, *, limit: int = 500) -> list[User]: """Fuzzy-match users by a substring of their email local part. Backs the admin LLM-metrics username filter — returns every account whose username contains ``substring`` (case-insensitive). """ return await self._repo.find_users_by_email_local_part_like(substring, limit=limit) async def create_user(self, email: str, password: str | None = None, system_role: str = "user", needs_setup: bool = False, approved: bool = True) -> User: """Create a new local user. Args: email: User email address password: Plain text password (will be hashed) system_role: Role to assign ("admin" or "user") needs_setup: If True, user must complete setup on first login approved: Login-whitelist 放行标记. **黑名单模式**:新账号默认 ``True`` (从未登录过的用户首次登录即放行),管理员可在白名单管理页对个别 用户「取消放行」(置 ``False``) 来封禁。仅当 ``login_whitelist.enabled`` 打开时 ``approved=False`` 才会被拦。 Returns: Created User instance """ password_hash = await hash_password_async(password) if password else None user = User( email=email, password_hash=password_hash, system_role=system_role, needs_setup=needs_setup, approved=approved, ) return await self._repo.create_user(user) async def get_user_by_oauth(self, provider: str, oauth_id: str) -> User | None: """Get user by OAuth provider and ID.""" return await self._repo.get_user_by_oauth(provider, oauth_id) async def count_users(self) -> int: """Return total number of registered users.""" return await self._repo.count_users() async def count_admin_users(self) -> int: """Return number of admin users.""" return await self._repo.count_admin_users() async def update_user(self, user: User) -> User: """Update an existing user.""" return await self._repo.update_user(user) async def list_users(self) -> list[User]: """Return all registered users ordered by creation date.""" return await self._repo.list_users() async def get_user_by_email(self, email: str) -> User | None: """Get user by email.""" return await self._repo.get_user_by_email(email)