Files

147 lines
6.0 KiB
Python

"""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)