feat(demo): allow chart display-pref toggles under demo mode

The DEMO_MODE block 403s every non-GET /api/* request outside a narrow
allowlist, which also caught the above-chart display toggles — the
chart-overlay switches and the comparison-baseline selector — even
though they only write the caller's own dashboard-prefs blob.

Admit PUT /api/dashboard/chart-overlay-prefs and PUT
/api/dashboard/widgets in demo mode. The allowlist now pins method per
path, so the widgets PUT does not drag in the layout-reset DELETE. The
block only runs under DEMO_MODE=true, so production is unaffected.
This commit is contained in:
Marc-André Bombeck committed 2026-06-07 23:33:19 +02:00
1 parent 384ca762e8
commit bea6b9fed4
2 files changed
+100 -6

No files matched your search

+25 -6
View File
@@ -83,12 +83,29 @@ function isPublicPath(pathname: string): boolean {
/**
* API paths that are allowed to accept mutations (POST/PUT/PATCH/DELETE)
* even when DEMO_MODE is enabled. Everything else is read-only.
*
* Each entry pins the exact path AND the exact method, so admitting one
* verb on a route never opens its siblings (e.g. the dashboard-widgets
* PUT must not drag in the layout-reset DELETE on the same path).
*
* The login family is the historical baseline — a demo visitor still has
* to authenticate. The two dashboard entries are display-only preference
* writes: idempotent, user-scoped, Zod-validated, and touching nothing but
* the caller's own `User.dashboardWidgetsJson` blob (chart-overlay toggles
* + comparison-baseline selector). They carry no health data and are safe
* to exercise in the demo so the above-chart toggles work there. This whole
* block only runs under `DEMO_MODE=true`, so production (apps01) is
* unaffected by construction.
*/
const DEMO_MUTATION_ALLOWLIST = [
"/api/auth/login",
"/api/auth/passkey/login-options",
"/api/auth/passkey/login-verify",
];
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" },
// Chart display-pref toggles above the charts (dashboard + insights):
{ path: "/api/dashboard/chart-overlay-prefs", method: "PUT" },
{ path: "/api/dashboard/widgets", method: "PUT" },
];
// Legacy route redirects (German → English)
const LEGACY_REDIRECTS: Record<string, string> = {
@@ -170,7 +187,9 @@ export function proxy(request: NextRequest) {
if (
isApi &&
isMutation &&
!DEMO_MUTATION_ALLOWLIST.some((p) => pathname === p)
!DEMO_MUTATION_ALLOWLIST.some(
(entry) => pathname === entry.path && method === entry.method,
)
) {
return NextResponse.json(
{