Split databases and Redis — prepare infrastructure for per-domain isolation

Redis: per-app DB index (0-5) with shared auth DB 15 for SSO keys;
flushdb replaces flushall so deploys don't wipe cross-app auth state.

Postgres: drop 13 cross-domain FK constraints (migration v2t0p8q9r0),
remove dead ORM relationships, add explicit joins for 4 live ones.
Multi-engine sessions (account + federation) ready for per-domain DBs
via DATABASE_URL_ACCOUNT / DATABASE_URL_FEDERATION env vars.

All URLs initially point to the same appdb — zero behaviour change
until split-databases.sh is run to migrate data to per-domain DBs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-25 02:20:34 +00:00
parent 57d2a6a6e3
commit 580f551700
25 changed files with 459 additions and 102 deletions

View File

@@ -162,17 +162,20 @@ def create_base_app(
uid = qs.get("uid")
grant_token = qs.get("grant_token")
from shared.browser.app.redis_cacher import get_redis
redis = get_redis()
from shared.infrastructure.auth_redis import get_auth_redis
try:
auth_redis = await get_auth_redis()
except Exception:
auth_redis = None
# Case 1: logged in — verify grant still valid (direct DB, cached)
if uid and grant_token:
cache_key = f"grant:{grant_token}"
if redis:
if auth_redis:
# Quick check: if did_auth was cleared (logout), skip cache
device_id = g.device_id
did_auth_present = await redis.get(f"did_auth:{device_id}") if device_id else True
cached = await redis.get(cache_key)
did_auth_present = await auth_redis.get(f"did_auth:{device_id}") if device_id else True
cached = await auth_redis.get(cache_key)
if cached == b"ok" and did_auth_present:
return
if cached == b"revoked":
@@ -183,10 +186,10 @@ def create_base_app(
return
from sqlalchemy import select
from shared.db.session import get_session
from shared.db.session import get_account_session
from shared.models.oauth_grant import OAuthGrant
try:
async with get_session() as s:
async with get_account_session() as s:
grant = await s.scalar(
select(OAuthGrant).where(OAuthGrant.token == grant_token)
)
@@ -194,8 +197,8 @@ def create_base_app(
except Exception:
return # DB error — don't log user out
if redis:
await redis.set(cache_key, b"ok" if valid else b"revoked", ex=60)
if auth_redis:
await auth_redis.set(cache_key, b"ok" if valid else b"revoked", ex=60)
if not valid:
qs.pop("uid", None)
qs.pop("grant_token", None)
@@ -214,8 +217,8 @@ def create_base_app(
# Check if account signalled a login after we cached "not logged in"
# (blog_did == account_did — same value set during OAuth callback)
if device_id and redis and pnone_at:
auth_ts = await redis.get(f"did_auth:{device_id}")
if device_id and auth_redis and pnone_at:
auth_ts = await auth_redis.get(f"did_auth:{device_id}")
if auth_ts:
try:
if float(auth_ts) > pnone_at:
@@ -226,8 +229,8 @@ def create_base_app(
if pnone_at and (now - pnone_at) < 300:
return
if device_id and redis:
cached = await redis.get(f"prompt:{name}:{device_id}")
if device_id and auth_redis:
cached = await auth_redis.get(f"prompt:{name}:{device_id}")
if cached == b"none":
return
return redirect(f"/auth/login?prompt=none&next={_quote(request.url, safe='')}")
@@ -279,6 +282,8 @@ def create_base_app(
@app.after_serving
async def _stop_event_processor():
await _event_processor.stop()
from shared.infrastructure.auth_redis import close_auth_redis
await close_auth_redis()
# --- favicon ---
@app.get("/favicon.ico")