feat(auth): per-tab account — different account per browser tab
Two tabs in one browser can now run two different signed-in accounts at once. - The signed session cookie stays the browser's account WALLET (account_ids). Which account a given tab acts as is a per-tab choice held in sessionStorage and sent per-request via the X-Siftlode-Account header; current_user honours it only for an account already in the wallet, without mutating the cookie's default account. Switching accounts sets the header + reloads THIS tab only, instead of the old cookie-wide switch that changed every tab. - WebSocket can't send headers, so the per-tab account rides in the ?account= query param (validated against the wallet). - Logout is per-tab aware: it signs the requesting tab's active account out of the wallet (promoting a new default only if the removed one was the default), and the tab drops its override. A stale per-tab header account 401s just that tab instead of clearing the session. - Serve index.html with Cache-Control: no-cache so a deploy's new hashed bundle is picked up immediately instead of the browser running a heuristically-cached stale index.html.
This commit is contained in:
parent
c0487101fe
commit
5cd807ec51
6 changed files with 146 additions and 36 deletions
|
|
@ -131,6 +131,13 @@ app.include_router(setup_routes.router)
|
|||
|
||||
# The built SPA (populated by the Docker frontend build stage).
|
||||
STATIC_DIR = Path(__file__).parent / "static_spa"
|
||||
# index.html is unhashed and references the content-hashed /assets bundles, so it MUST NOT be
|
||||
# heuristically cached — otherwise, after a deploy, a browser keeps serving the old index.html
|
||||
# (pointing at the previous bundle) and runs stale code until a hard refresh. `no-cache` lets it
|
||||
# stay cached but forces revalidation (cheap 304 via the FileResponse ETag) on every load. The
|
||||
# hashed bundles under /assets can cache forever (a new build changes their filename).
|
||||
INDEX_HTML = STATIC_DIR / "index.html"
|
||||
INDEX_HEADERS = {"Cache-Control": "no-cache"}
|
||||
app.mount(
|
||||
"/assets",
|
||||
StaticFiles(directory=STATIC_DIR / "assets", check_dir=False),
|
||||
|
|
@ -140,7 +147,7 @@ app.mount(
|
|||
|
||||
@app.get("/")
|
||||
async def index() -> FileResponse:
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
return FileResponse(INDEX_HTML, headers=INDEX_HEADERS)
|
||||
|
||||
|
||||
@app.get("/{full_path:path}")
|
||||
|
|
@ -155,4 +162,4 @@ async def spa_fallback(full_path: str) -> FileResponse:
|
|||
candidate = (STATIC_DIR / full_path).resolve()
|
||||
if candidate.is_file() and STATIC_DIR.resolve() in candidate.parents:
|
||||
return FileResponse(candidate)
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
return FileResponse(INDEX_HTML, headers=INDEX_HEADERS)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue