From 93c47b38aa5d8f3cdbba7d2efdbf2a5653f9441a Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 17 Jun 2026 20:15:58 -0400 Subject: [PATCH] fix(19): CR-02 send initialPassword + map 409 conflict in fetchCreateMember --- apps/pwa/src/api/client.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/apps/pwa/src/api/client.ts b/apps/pwa/src/api/client.ts index 743acd9..2c054c9 100644 --- a/apps/pwa/src/api/client.ts +++ b/apps/pwa/src/api/client.ts @@ -168,10 +168,14 @@ export async function fetchChangePassword(body: { * POST /api/admin/members — create a new local member account (Phase 19, Surface 11A). * Admin-only; server enforces requireAdmin. * + * Request contract: the server's createMemberSchema requires + * { displayName, username, initialPassword } + * (see apps/api/src/routes/admin.ts). The caller-facing `password` field is mapped to + * `initialPassword` here so the request validates server-side. + * * Status codes: - * 409 → username already taken - * 422 → validation failure (short password / mismatch) - * other non-ok → generic error + * 409 → username already taken (throws Error with message 'conflict') + * other non-ok → generic error (throws Error('server')) */ export async function fetchCreateMember(body: { displayName: string; @@ -183,10 +187,19 @@ export async function fetchCreateMember(body: { headers: { 'Content-Type': 'application/json' }, credentials: 'include', redirect: 'manual', - body: JSON.stringify(body), + // CR-02: the server expects `initialPassword`, not `password`. Send the field it + // validates against — otherwise Zod rejects every create with a generic 400. + body: JSON.stringify({ + displayName: body.displayName, + username: body.username, + initialPassword: body.password, + }), }); if (res.type === 'opaqueredirect' || res.status === 401) throw new SessionExpiredError(); + // 409 → username conflict. The server returns { error: 'Username already in use' } (no + // `code` field), so map the status to the 'conflict' sentinel the AdminPage handler expects. + if (res.status === 409) throw new Error('conflict'); if (!res.ok) { const detail = (await res.json().catch(() => ({}))) as { code?: string }; throw new Error(detail.code ?? 'server');