mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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:
1 parent
384ca762e8
commit
bea6b9fed4
2 files changed
+100
-6
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
/**
|
||||
* Demo-mode mutation allowlist. On a `DEMO_MODE=true` deploy the proxy
|
||||
* 403s every non-GET `/api/*` request except a narrow allowlist. Login
|
||||
* is the historical baseline; the two dashboard display-pref writes
|
||||
* (chart-overlay toggles + comparison-baseline selector) were added so
|
||||
* the above-chart toggles work in the demo. They touch nothing but the
|
||||
* caller's own `User.dashboardWidgetsJson` blob.
|
||||
*
|
||||
* This guard locks the allowlist shape in place so a refactor can't
|
||||
* silently widen it (admitting a data-bearing mutation) or narrow it
|
||||
* (re-breaking the demo toggles). The whole block is gated on
|
||||
* `DEMO_MODE === "true"`, so production is unaffected by construction —
|
||||
* the final case proves the gate.
|
||||
*/
|
||||
|
||||
vi.mock("@/lib/process-type", () => ({
|
||||
shouldRunWeb: () => true,
|
||||
}));
|
||||
|
||||
import { proxy } from "../proxy";
|
||||
|
||||
function makeRequest(pathname: string, method: string): NextRequest {
|
||||
return new NextRequest(`http://localhost${pathname}`, { method });
|
||||
}
|
||||
|
||||
const ORIGINAL_DEMO_MODE = process.env.DEMO_MODE;
|
||||
|
||||
describe("proxy.ts DEMO_MODE mutation allowlist", () => {
|
||||
beforeEach(() => {
|
||||
process.env.DEMO_MODE = "true";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (ORIGINAL_DEMO_MODE === undefined) {
|
||||
delete process.env.DEMO_MODE;
|
||||
} else {
|
||||
process.env.DEMO_MODE = ORIGINAL_DEMO_MODE;
|
||||
}
|
||||
});
|
||||
|
||||
it("permits the chart-overlay-prefs PUT (above-chart display toggles)", () => {
|
||||
const res = proxy(
|
||||
makeRequest("/api/dashboard/chart-overlay-prefs", "PUT"),
|
||||
);
|
||||
expect(res.status).not.toBe(403);
|
||||
});
|
||||
|
||||
it("permits the dashboard-widgets PUT (comparison-baseline selector)", () => {
|
||||
const res = proxy(makeRequest("/api/dashboard/widgets", "PUT"));
|
||||
expect(res.status).not.toBe(403);
|
||||
});
|
||||
|
||||
it("still blocks a health-data mutation (POST /api/measurements)", () => {
|
||||
const res = proxy(makeRequest("/api/measurements", "POST"));
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it("does not drag in the layout-reset DELETE on the allowlisted widgets path", () => {
|
||||
// The allowlist pins method per path; admitting the widgets PUT
|
||||
// must not open DELETE (which wipes the user's dashboard layout).
|
||||
const res = proxy(makeRequest("/api/dashboard/widgets", "DELETE"));
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it("is inert when DEMO_MODE is off — production is unaffected", () => {
|
||||
delete process.env.DEMO_MODE;
|
||||
// With the demo gate off, the proxy never short-circuits an API
|
||||
// mutation; the route's own auth/handler runs instead.
|
||||
const res = proxy(makeRequest("/api/measurements", "POST"));
|
||||
expect(res.status).not.toBe(403);
|
||||
});
|
||||
});
|
||||
+25
-6
@@ -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(
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user