"""Local email/password authentication provider.""" import logging from app.gateway.auth.models import User from app.gateway.auth.password import hash_password_async, verify_account_password 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 not await verify_account_password(password, user): return None 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 get_users_by_ids(self, user_ids: list[str]) -> list[User]: """Resolve many account owners with one repository read.""" return await self._repo.get_users_by_ids(user_ids) 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, login_password=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 create_initial_admin(self, email: str, password: str) -> User: hashed = await hash_password_async(password) user = User(email=email.strip().lower(), password_hash=hashed, login_password=hashed, system_role="admin", approved=True) return await self._repo.create_initial_admin(user) 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 update_credentials(self, user: User, password_hash: str | None, *, email: str | None = None) -> User: """Rotate both password representations and invalidate prior sessions.""" return await self._repo.update_credentials(user, password_hash, email=email) async def start_login_session(self, user: User) -> User: return await self._repo.start_login_session(user) async def list_users(self) -> list[User]: """Return all registered users ordered by creation date.""" return await self._repo.list_users() async def list_users_page( self, *, q: str | None = None, limit: int = 20, offset: int = 0 ) -> list[User]: """Return one page of users (optionally filtered by ``q``).""" return await self._repo.list_users_page(q=q, limit=limit, offset=offset) async def count_users_filtered( self, *, q: str | None = None, approved_or_admin: bool = False ) -> int: """Count users with the same ``q`` filter; optionally approved/admin only.""" return await self._repo.count_users_filtered(q=q, approved_or_admin=approved_or_admin) async def get_user_by_email(self, email: str) -> User | None: """Get user by email.""" return await self._repo.get_user_by_email(email)