siftlode/backend/app/routes/setup.py
npeter83 eda9bbbc57 feat(setup): first-boot detection + setup-mode gating + one-time token (epic 6a)
- app_state gains configured + setup_token_hash (migration 0025); existing installs (any users)
  are marked configured so they never see the wizard, a fresh DB starts in setup mode.
- On first boot the app mints a one-time setup token and logs the wizard URL (only the hash is
  stored); the token gates the setup steps (added in 6c).
- A middleware locks all /api + /auth routes (except /api/setup, /api/version, /healthz, static)
  to 503 until configured, with a DB-free cached fast-path once setup completes.
- GET /api/setup/status tells the SPA whether to show the wizard.
2026-06-21 00:38:47 +02:00

32 lines
1.3 KiB
Python

"""First-run install wizard.
`GET /api/setup/status` is public (the SPA uses it to decide whether to show the wizard). Every
mutating step goes through `require_setup`: it rejects once the instance is configured AND requires
the one-time setup token (printed to the container logs at first boot, carried by the wizard as the
X-Setup-Token header). The wizard steps themselves are added in epic 6c.
"""
from fastapi import APIRouter, Depends, Header, HTTPException
from sqlalchemy.orm import Session
from app import state
from app.db import get_db
router = APIRouter(prefix="/api/setup", tags=["setup"])
def require_setup(
x_setup_token: str | None = Header(default=None),
db: Session = Depends(get_db),
) -> Session:
"""Gate for setup steps. Returns the db session for the handler to reuse."""
if state.is_configured(db):
raise HTTPException(status_code=409, detail="This instance is already set up.")
if not state.check_setup_token(db, x_setup_token):
raise HTTPException(status_code=403, detail="Invalid or missing setup token.")
return db
@router.get("/status")
def setup_status(db: Session = Depends(get_db)) -> dict:
"""Public: whether the instance still needs first-run setup."""
return {"configured": state.is_configured(db)}