Phase 19: Local Auth (No-OIDC Mode) #23

Merged
luckberg merged 78 commits from gsd/phase-19-local-auth-no-oidc-mode into main 2026-06-18 06:25:00 -04:00
Showing only changes of commit 93c47b38aa - Show all commits
+17 -4
View File
@@ -168,10 +168,14 @@ export async function fetchChangePassword(body: {
* POST /api/admin/members — create a new local member account (Phase 19, Surface 11A). * POST /api/admin/members — create a new local member account (Phase 19, Surface 11A).
* Admin-only; server enforces requireAdmin. * 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: * Status codes:
* 409 → username already taken * 409 → username already taken (throws Error with message 'conflict')
* 422 → validation failure (short password / mismatch) * other non-ok → generic error (throws Error('server'))
* other non-ok → generic error
*/ */
export async function fetchCreateMember(body: { export async function fetchCreateMember(body: {
displayName: string; displayName: string;
@@ -183,10 +187,19 @@ export async function fetchCreateMember(body: {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include', credentials: 'include',
redirect: 'manual', 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(); 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) { if (!res.ok) {
const detail = (await res.json().catch(() => ({}))) as { code?: string }; const detail = (await res.json().catch(() => ({}))) as { code?: string };
throw new Error(detail.code ?? 'server'); throw new Error(detail.code ?? 'server');