fix(demo): stop refusing a visitor's own sign-out, language and timings

On the demo every page logged its own failures. The sign-out answered
403, so a visitor could not sign out at all; the language write answered
403 on every page load, and because its once-per-tab marker is only set
on success it was sent again on the next one; the page-timing beacon was
refused the same way. Five password sign-ins per 15 minutes per address
locked out a group trying the published demo account together.

Sign-out, the language write and the timing beacon now pass the demo
block: each touches only the visitor. The language route still answers
and sets the visitor's cookie, but under DEMO_MODE it no longer writes
the shared account's row, so one visitor's choice is not the next one's
starting state. A demo allows 30 password sign-ins per 15 minutes per
address; other instances keep 5.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 21:27:03 +01:00
1 parent 62b9e404ef
commit 3385163fd6
7 files changed
+137 -12

No files matched your search

+7 -3
View File
@@ -183,6 +183,11 @@ const DEMO_MUTATION_ALLOWLIST: ReadonlyArray<{ path: string; method: string }> =
{ path: "/api/auth/login", method: "POST" },
{ path: "/api/auth/passkey/login-options", method: "POST" },
{ path: "/api/auth/passkey/login-verify", method: "POST" },
// Each touches only this visitor: the session, the language cookie (the
// route leaves the shared row alone under DEMO_MODE), a metrics log line.
{ path: "/api/auth/logout", method: "POST" },
{ path: "/api/auth/me/locale", method: "PUT" },
{ path: "/api/internal/web-vitals", method: "POST" },
// Chart display-pref toggles above the charts (dashboard + insights):
{ path: "/api/dashboard/chart-overlay-prefs", method: "PUT" },
{ path: "/api/dashboard/widgets", method: "PUT" },
@@ -231,8 +236,7 @@ const DEMO_MUTATION_ALLOWLIST: ReadonlyArray<{ path: string; method: string }> =
// The per-profile routes (`/api/managed-profiles/{id}`, its guardians)
// are different paths and stay closed too.
{ path: "/api/auth/profile", method: "PUT" },
// v1.42.2 — the record search is a read that travels as a POST so the
// query stays out of URLs and access logs. It writes nothing.
// The record search: a read sent as a POST to keep the query out of logs.
{ path: "/api/search", method: "POST" },
];
@@ -473,7 +477,7 @@ export function proxy(request: NextRequest) {
);
}
// Demo mode: block all mutations except login
// Demo mode: block every mutation outside the allowlist.
if (process.env.DEMO_MODE === "true") {
const method = request.method.toUpperCase();
const isMutation =