fix(12-04): align setup config client to camelCase API contract + readable errors

BUG 1: Rename SetupConfigPayload fields from snake_case to camelCase to match
the API configSchema (appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey).
Update SetupPage.tsx handleSaveAndValidate to send the correct camelCase keys.

BUG 2: Extract human-readable message from ZodError object in postSetupConfig
error handler. When body.error is an object with issues[], use issues[0].message
instead of stringifying the object (which produces "[object Object]").

All 245 PWA tests pass; TypeScript clean.
This commit is contained in:
Lucas Berger
2026-06-15 14:48:07 -04:00
parent 9f20c8b7cc
commit 120ce85a59
2 changed files with 31 additions and 10 deletions
+27 -6
View File
@@ -543,12 +543,15 @@ export interface SetupStatusResponse {
* Payload for POST /api/setup/config.
* Collects non-secret runtime config written to the app_config table (D-02).
* No secrets — VAPID private key and encryption key stay in Docker env.
*
* Field names match the API's configSchema exactly (camelCase).
* API contract: { oidcIssuer, oidcClientId, vapidPublicKey, appExternalUrl }
*/
export interface SetupConfigPayload {
app_url: string;
oidc_issuer: string;
oidc_client_id: string;
vapid_public_key: string;
appExternalUrl: string;
oidcIssuer: string;
oidcClientId: string;
vapidPublicKey: string;
}
/**
@@ -594,8 +597,26 @@ export async function postSetupConfig(payload: SetupConfigPayload): Promise<void
}
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(body.error ?? `POST /api/setup/config failed: ${res.status}`);
const body = (await res.json().catch(() => ({}))) as {
error?: string | { name?: string; issues?: Array<{ message: string }> };
};
// body.error may be a ZodError object ({ name: "ZodError", issues: [...] })
// rather than a plain string — extract the first issue message to avoid
// "[object Object]" appearing in the UI (BUG 2 fix).
let message: string;
if (typeof body.error === 'string') {
message = body.error;
} else if (
body.error &&
typeof body.error === 'object' &&
Array.isArray((body.error as { issues?: unknown[] }).issues) &&
(body.error as { issues: Array<{ message: string }> }).issues.length > 0
) {
message = (body.error as { issues: Array<{ message: string }> }).issues[0].message;
} else {
message = `POST /api/setup/config failed: ${res.status}`;
}
throw new Error(message);
}
}
+4 -4
View File
@@ -502,10 +502,10 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) {
}
configMutation.mutate({
app_url: appUrl.trim(),
oidc_issuer: oidcIssuer.trim(),
oidc_client_id: oidcClientId.trim(),
vapid_public_key: vapidPublicKey.trim(),
appExternalUrl: appUrl.trim(),
oidcIssuer: oidcIssuer.trim(),
oidcClientId: oidcClientId.trim(),
vapidPublicKey: vapidPublicKey.trim(),
});
}