Files
qtalker---/backend/app/main.py
Indiana 7f0775c8c3 feat: periodically sweep expired auth sessions
auth_sessions rows were never deleted after expiry, only rejected
on read. Adds a background sweep (every 30 min) in the app lifespan,
plus a tested pure delete_expired_sessions() function.
2026-07-21 03:43:34 +00:00

90 lines
2.7 KiB
Python

import asyncio
import contextlib
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
import app.models # noqa: F401 — registers models on Base.metadata before create_all
from app.config import settings
from app.db import Base, async_session_maker, engine
from app.routes.auth import router as auth_router
from app.routes.codex import router as codex_router
from app.routes.shop import router as shop_router
from app.session_cleanup import delete_expired_sessions
from app.ws import AUDIO_DIR
from app.ws import router as ws_router
FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
SESSION_CLEANUP_INTERVAL_SECONDS = 30 * 60
async def _session_cleanup_loop() -> None:
"""Periodically sweeps expired auth_sessions rows so the table doesn't
grow forever — get_current_user already rejects expired sessions on
read, this just deletes the rows themselves."""
try:
while True:
await asyncio.sleep(SESSION_CLEANUP_INTERVAL_SECONDS)
try:
async with async_session_maker() as db:
await delete_expired_sessions(db)
except Exception:
# A transient DB hiccup shouldn't kill the sweep loop —
# just try again next interval.
pass
except asyncio.CancelledError:
pass
@asynccontextmanager
async def lifespan(app: FastAPI):
AUDIO_DIR.mkdir(parents=True, exist_ok=True)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
cleanup_task = asyncio.create_task(_session_cleanup_loop())
try:
yield
finally:
cleanup_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await cleanup_task
app = FastAPI(title="Quantumancy", lifespan=lifespan)
app.include_router(auth_router)
app.include_router(codex_router)
app.include_router(shop_router)
app.include_router(ws_router)
@app.get("/healthz")
async def healthz():
return {"status": "ok"}
app.mount(
"/assets",
StaticFiles(directory=FRONTEND_DIST / "assets", check_dir=False),
name="frontend-assets",
)
app.mount(
"/audio",
StaticFiles(directory=AUDIO_DIR, check_dir=False),
name="spirit-audio",
)
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
index_file = FRONTEND_DIST / "index.html"
if not index_file.exists():
raise HTTPException(
status_code=503,
detail="Frontend not built. Run `npm run build` in frontend/ and restart.",
)
return FileResponse(index_file)