"""Data access for the identity domain. **Only this module may touch id_* tables.**""" from __future__ import annotations from datetime import UTC, datetime from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from packages.shared.models import Consent, User, UserIdentity, UserSession async def get_user(session: AsyncSession, user_id: int) -> User | None: return await session.get(User, user_id) async def find_identity( session: AsyncSession, *, provider: str, union_id: str ) -> UserIdentity | None: result = await session.execute( select(UserIdentity).where( UserIdentity.provider == provider, UserIdentity.union_id == union_id ) ) return result.scalar_one_or_none() def add_user(session: AsyncSession, user: User) -> User: session.add(user) return user def add_identity(session: AsyncSession, identity: UserIdentity) -> UserIdentity: session.add(identity) return identity def add_consent(session: AsyncSession, consent: Consent) -> Consent: session.add(consent) return consent async def find_session_by_hash(session: AsyncSession, token_hash: str) -> UserSession | None: # UNIQUE(refresh_token_hash) 保证至多一行,scalar_one_or_none 即可 result = await session.execute( select(UserSession).where(UserSession.refresh_token_hash == token_hash) ) return result.scalar_one_or_none() def add_session(session: AsyncSession, user_session: UserSession) -> UserSession: session.add(user_session) return user_session async def revoke_session(session: AsyncSession, session_id: int) -> bool: """撤销单条会话。返回 False = 该行不存在或已被撤销。 WHERE 里带 ``revoked = false`` 再 FOR UPDATE(照 billing.repo 的 lock_or_create_quota 模式):两个并发请求拿着同一 refresh 轮换时, 后到者等先到者提交后重估条件、匹配 0 行返回 False——调用方据此按 「已撤销/重放」处理,无需自己加锁。 """ result = await session.execute( select(UserSession) .where(UserSession.session_id == session_id, UserSession.revoked.is_(False)) .with_for_update() ) row = result.scalar_one_or_none() if row is None: return False row.revoked = True row.revoked_at = datetime.now(UTC) return True async def revoke_all_sessions(session: AsyncSession, user_id: int) -> int: """撤销某用户全部会话(logout-all)。返回实际撤销的行数。 V1 先备着:对应 FR 后续的「退出全部设备」,本批次不接路由,留实现是为了 把「会话可撤销」的数据通路一次铺好。 """ result = await session.execute( select(UserSession) .where(UserSession.user_id == user_id, UserSession.revoked.is_(False)) .with_for_update() ) now = datetime.now(UTC) revoked_count = 0 for row in result.scalars(): row.revoked = True row.revoked_at = now revoked_count += 1 return revoked_count