"""Kurzlebige Merkzettel fuer den Browser-Anmeldevorgang. ``state``, ``nonce`` und der PKCE-Verifier muessen den Ausflug zum Anmeldedienst ueberdauern. Sie liegen dafuer in der Datenbank statt in einem Cookie – warum, steht bei :class:`~app.models.OidcLoginState`. Reine Datenbank-Funktionen, damit sie sich mit der vorhandenen ``db``-Fixture pruefen lassen. """ from __future__ import annotations import secrets from datetime import datetime, timedelta, timezone from sqlalchemy.orm import Session from ..models import OidcLoginState # Genug fuer eine Anmeldung samt Zwei-Faktor-Eingabe, kurz genug, dass ein # liegengebliebener Zettel nicht ewig gilt. GUELTIG = timedelta(minutes=15) def _jetzt() -> datetime: return datetime.now(timezone.utc) def _mit_zeitzone(wert: datetime) -> datetime: # SQLite gibt Zeitstempel ohne Zonenangabe zurueck; ein Vergleich mit einem # bewussten Zeitpunkt wuerfe sonst einen TypeError. return wert if wert.tzinfo is not None else wert.replace(tzinfo=timezone.utc) def anlegen( db: Session, provider: str, link_user_id: int | None = None ) -> OidcLoginState: """Erzeugt state, nonce und PKCE-Verifier und merkt sie vor. ``link_user_id`` macht daraus einen Verknuepfungs- statt Anmeldevorgang. """ zettel = OidcLoginState( state=secrets.token_urlsafe(32), provider=provider, nonce=secrets.token_urlsafe(24), # RFC 7636 erlaubt 43-128 Zeichen; token_urlsafe(64) liefert 86. code_verifier=secrets.token_urlsafe(64), link_user_id=link_user_id, created_at=_jetzt(), ) db.add(zettel) db.commit() return zettel def einloesen(db: Session, state: str, jetzt: datetime | None = None) -> OidcLoginState | None: """Holt den Zettel und entfernt ihn dabei – er gilt genau einmal. Damit ist ein zweiter Rueckweg mit demselben ``state`` wirkungslos. """ zettel = db.get(OidcLoginState, state) if zettel is None: return None # Vor dem Ablauf-Vergleich loeschen: auch ein abgelaufener Zettel ist # verbraucht und soll nicht liegenbleiben. db.delete(zettel) db.commit() if _mit_zeitzone(zettel.created_at) + GUELTIG < (jetzt or _jetzt()): return None return zettel def aufraeumen(db: Session, jetzt: datetime | None = None) -> int: """Entfernt abgelaufene Zettel (abgebrochene Anmeldungen).""" grenze = (jetzt or _jetzt()) - GUELTIG alte = [z for z in db.query(OidcLoginState).all() if _mit_zeitzone(z.created_at) < grenze] for zettel in alte: db.delete(zettel) if alte: db.commit() return len(alte)