fix(demo): the profile route writes only the baseline fields in the demo

Allowlisting `PUT /api/auth/profile` so the setup flow's baseline step could
finish also handed every visitor the shared demo account's contact email, its
display name, its full name and its insurer fields: the allowlist admits a
path and a method, and every field the handler behind it writes comes with
them. The justification beside the entry named three fields; the entry
admitted eleven.

The write is narrowed on the server, in `applyProfileUpdate`, to height, date
of birth and sex while `DEMO_MODE` is set — not in the client, which is the
party the allowlist exists to distrust. It happens ahead of the email conflict
check, which would otherwise answer "is this address registered here?" to
anybody who can open the demo. The display-name box goes read-only there, as
the anamnesis card already is, so nothing accepts input it will discard.

The entry's comment now lists what the handler writes and what is admitted.
This commit is contained in:
Marc-André Bombeck committed 2026-09-11 12:22:47 +02:00
1 parent ceefbc5571
commit 45599c527e
7 files changed
+338 -6

No files matched your search

@@ -144,4 +144,21 @@ describe("<ConfirmScreen> — the anamnesis card on a shared demo account", () =
expect(html).toContain('data-slot="onboarding-anamnesis"');
expect(html).toContain('data-readonly="true"');
});
it("takes no display name in the demo either", () => {
// `PUT /api/auth/profile` is allowlisted for the demo, but the server
// drops every field outside height / date of birth / sex — the display
// name would otherwise be the next visitor's. A box that accepts input
// and silently discards it is the same dishonesty as a failed save, so
// it does not accept input.
const html = render(state(), {}, true);
expect(html).toMatch(/id="ob-baseline-display-name"[^>]*disabled/);
// The three the step exists for still take their answers.
expect(html).not.toMatch(/id="ob-baseline-height"[^>]*disabled/);
});
it("takes a display name on an ordinary instance", () => {
const html = render(state());
expect(html).not.toMatch(/id="ob-baseline-display-name"[^>]*disabled/);
});
});
@@ -60,6 +60,7 @@ export function BaselineFields({
onChange,
heightAdapter,
errors,
readOnlyDisplayName = false,
}: {
value: BaselineFieldValues;
onChange: <K extends keyof BaselineFieldValues>(
@@ -68,6 +69,15 @@ export function BaselineFields({
) => void;
heightAdapter: HeightUnitAdapter;
errors: BaselineFieldErrors;
/**
* v1.39 — the instance cannot persist a display name, so the box does not
* take one. True under `DEMO_MODE`, where `applyProfileUpdate` writes only
* height, date of birth and sex: the demo's single published account means
* a display name typed here would be the next visitor's. A field that
* accepts input and silently discards it is the same dishonesty as a save
* that fails, which is why it is disabled rather than merely ignored.
*/
readOnlyDisplayName?: boolean;
}) {
const { t } = useTranslations();
@@ -85,6 +95,7 @@ export function BaselineFields({
id={IDS.displayName}
value={value.displayName}
onChange={(e) => onChange("displayName", e.target.value)}
disabled={readOnlyDisplayName}
autoComplete="nickname"
maxLength={50}
placeholder={t("onboarding.baseline.displayNamePlaceholder")}
+5 -2
View File
@@ -117,8 +117,10 @@ export function BaselineForm({
confirming?: boolean;
/**
* True on a `DEMO_MODE` instance, where the shared account's self-context
* write is off the edge allowlist. Makes the anamnesis card read-only so
* the refusal never reaches a visitor as a failed confirm.
* write is off the edge allowlist and its profile write is narrowed to the
* three baseline fields. Makes the anamnesis card and the display-name box
* read-only, so neither refusal reaches a visitor as a failed confirm or as
* an answer that silently vanished.
*/
demoMode?: boolean;
}) {
@@ -253,6 +255,7 @@ export function BaselineForm({
onChange={patch}
heightAdapter={heightAdapter}
errors={fieldErrors}
readOnlyDisplayName={demoMode}
/>
<AnamnesisCard