Lane 2: what the public pages read (#728) #752
Loading…
Reference in a new issue
No description provided.
Delete branch "build/lane2-server"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Lane 2 of the v4.5.0 redesign (#728), the first PR of its series: the server change the public pages need, so the UI PRs that follow are UI only (build plan §7). Backend only.
The public pages are the only part of Quest Board that reads the server rather than a campaign, and until now nothing said anything about a server without a session.
No migration
Instance settings are a key/JSON store (
app_settings), so "About this server" is a new key rather than a column. Worth saying plainly, because a lane that adds a settings concept usually means Alembic and this one does not.GET /public/site— the front doorIS-384,IS-385,IS-388.Every string is
nullrather than""when unset, because the pages draw a different sentence for "not said yet" than for "said, and empty".numbers: nullis the switch About this server (ADMIN-15, lane 9) turns off; zeros mean a server too new, which the page words itself — deciding when a number is worth saying is not the server's business.campaigns_runningexcludes soft-deleted and archived campaigns: an archived campaign is readable but is not running.sessions_playediscompleted, joined to the campaign, because a session has nodeleted_atof its own and a trashed campaign's sessions would otherwise keep counting.running_sinceismin(users.created_at). Nothing records when an instance was installed; the first account is created at the first sign-in, which is the closest thing the database holds. Named as a definition rather than invented as a column.blurbis the administrator's one line about their own server. It exists because the drawing's "Amberwake is run by Dana, for a few groups of friends" had no field behind it — and the UI PR had duly hard-coded it, which would have had every self-hosted Quest Board telling visitors that its operator runs it for a few groups of friends.GET /public/policies— privacy and licencesIS-387,IS-391,API-016,IS-388. The administrator's own words, the published notices address, and the retention settings read live rather than repeated — which is the whole point ofIS-387: a privacy page that repeats its server's retention posture starts lying the day that posture changes.API-016already exists asGET /api/content-packs/notice-contact, behindget_current_user. It stays where it is for the admin UI; this is a second, public reading of the same setting, because D64 puts that address on a page that has to be readable without an account.The invitation says two more things
IS-196, and the system line the drawing shows.InvitePreviewgainsgame_systemandrecords_sessions— the latter true only when the campaign has a Discord server connected and the instance has a bot configured, because anything looser would tell a stranger a table records when it has no way to.Nothing else about the endpoint changes: the code is still the secret, and a dead invite is still one 404 with one sentence for all five reasons.
?next=now means what the sign-in page says it meansSH-032. The rewritten sign-in page says "Your session ran out. Sign in again and you will come back to where you were."_safe_nexthonoured exactly two shapes —/auth/…and an invite link — so that sentence was true for an invitation and for nothing else in the app.It accepts any same-origin path now, and refuses everything else by rule rather than by allowlist, each rule with a test named after it: no leading
/, a protocol-relative//or/\, a backslash anywhere, any control character (a newline in aLocationheader being the interesting one), a scheme or authority, and a..segment.Three cases the old tests asserted as hostile now pass, and they are worth a reviewer's eye:
/dashboard— the point of the change./join/x?redirect=https://evil.example— accepted. It resolves same-origin; the absolute URL is inside the query string, which this function treats as opaque on purpose. Its guarantee is about the destination, never about what the destination does with its own parameters — a page that followed aredirect=of its own would be just as exploitable from a link in an email./join/../../admin— refused, although it cannot leave the origin. What makes it safe is dot-segment removal performed by the browser, not by us, and a non-browser client or an intermediary is not guaranteed to resolve it the same way. A path that says/join/…and lands on/adminis also a path that lies about where it goes.The two existing branches are byte-for-byte unchanged, and a value reaching them always begins
/a, so it can never be reclassified as protocol-relative.The two Discord link pages
EX-047,EX-048. Restyled to the design system's tokens in light and dark, and reworded to the drawing. They stay a second, standalone implementation, aslink_pages.pyalready explains: the backend renders them, outside the app, and sharing a component would mean shipping the app to draw two sentences.One out-of-scope fix, called out rather than smuggled
tests/test_audio_tasks.pycommitted a campaign and a completed session through its own session — invisible to the rolled-backdbfixture — and never deleted them. Nothing had ever run an unscoped count over campaigns or sessions, so the leak was silent;/public/site's numbers are exactly that query and it surfaced immediately, off by one. Fixed with thefinally-cleanup its sibling tests already use, rather than by loosening the new tests to tolerate stray global state.Checks
ruff format --checkandruff checkoverwebapp/backend/ scripts/ webapp/frontend/e2e/at the pinned 0.4.4: clean. Full backend suite: 3850 passed, 13 skipped.🤖 Generated with Claude Code