Phase 19: Local Auth (No-OIDC Mode) #23
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user