"""ЕСИА stand-in: the patient proves identity with the one-time connect code issued by the doctor. The doctor creates the code in «Подключить пациента» (identity DB `connect_codes`); the patient opens the QR deep link / enters the code. In prod this is ЕСИА OAuth; the `IdentityProvider` Protocol stays the same. """ import logging from datetime import UTC, timedelta from sqlalchemy.exc import SQLAlchemyError from app.core import clock from app.integrations.base import VerifiedPerson log = logging.getLogger(__name__) # One connect flow verifies the same code twice: the Telegram channel on `/start ` (to bind the chat) # and then the dialog engine's `new` state (to check the code belongs to this token). A short grace window # lets the second check pass; after it a used code is rejected like an unknown one. REDEEM_GRACE = timedelta(minutes=15) class ConnectCodeIdentity: """`IdentityProvider` over identity-DB connect codes.""" def __init__(self, grace: timedelta = REDEEM_GRACE) -> None: self.grace = grace async def verify(self, auth_code: str) -> VerifiedPerson | None: code = auth_code.strip() if not code: return None # Imported lazily: the integrations package is imported by clinical-side modules, which must not # pull the identity schema into their import graph (guardrail #5). from app.db import identity_repo from app.db.identity import ConnectCode from app.db.session import get_identity_session now = clock.now() try: async with get_identity_session() as s: row = await s.get(ConnectCode, code) if row is None: log.info("connect code rejected: unknown") return None person = VerifiedPerson(token=row.token, doctor_id=row.doctor_id) if row.used_at is not None: used_at = row.used_at if row.used_at.tzinfo else row.used_at.replace(tzinfo=UTC) if now - used_at <= self.grace: return person log.info("connect code rejected: already used") return None if await identity_repo.redeem_connect_code(s, code=code, now=now) is None: return None return person except (OSError, SQLAlchemyError) as exc: log.warning("connect code check failed error=%s", type(exc).__name__) return None