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.
This commit is contained in:
parent
d7b7acb637
commit
283c4c9a1e
5 changed files with 179 additions and 3 deletions
32
backend/app/routes/setup.py
Normal file
32
backend/app/routes/setup.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""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)}
|
||||
Loading…
Add table
Add a link
Reference in a new issue