"""Redis-хранилища state/PKCE для OAuth-потока (TTL = срок state в gnexus-gauth).
Redis вместо in-memory: переживает рестарт api-контейнера и
работает при нескольких воркерах uvicorn/gunicorn.
"""
import json
from datetime import datetime
from redis import Redis
from app.config import get_settings
STATE_PREFIX = "synapse:auth:state:"
PKCE_PREFIX = "synapse:auth:pkce:"
_redis: Redis | None = None
def get_redis() -> Redis:
global _redis
if _redis is None:
_redis = Redis.from_url(get_settings().redis_url, decode_responses=True)
return _redis
def _ttl(expires_at: datetime) -> int:
return max(1, int(expires_at.timestamp() - datetime.now(expires_at.tzinfo).timestamp()))
class RedisStateStore:
"""StateStoreInterface: state -> context (return_to, scopes)."""
def put(self, state: str, expires_at: datetime, context: dict | None = None) -> None:
get_redis().setex(STATE_PREFIX + state, _ttl(expires_at), json.dumps(context or {}))
def has(self, state: str) -> bool:
return bool(get_redis().exists(STATE_PREFIX + state))
def get_context(self, state: str) -> dict:
raw = get_redis().get(STATE_PREFIX + state)
return json.loads(raw) if raw else {}
def forget(self, state: str) -> None:
get_redis().delete(STATE_PREFIX + state)
class RedisPkceStore:
"""PkceStoreInterface: state -> PKCE verifier."""
def put(self, state: str, verifier: str, expires_at: datetime) -> None:
get_redis().setex(PKCE_PREFIX + state, _ttl(expires_at), verifier)
def get(self, state: str) -> str | None:
return get_redis().get(PKCE_PREFIX + state)
def forget(self, state: str) -> None:
get_redis().delete(PKCE_PREFIX + state)