diff --git a/artifacts/api-server/src/lib/setupService.ts b/artifacts/api-server/src/lib/setupService.ts index 86bc543a..2c406028 100644 --- a/artifacts/api-server/src/lib/setupService.ts +++ b/artifacts/api-server/src/lib/setupService.ts @@ -253,18 +253,31 @@ export async function completeInstall( return { ok: false, status: 409, error: "already_installed" }; } - // Username/email uniqueness re-check inside the txn. - const dup = await client.query<{ id: number }>( - `SELECT id FROM users WHERE username = $1 OR email = $2 LIMIT 1`, + // Username/email uniqueness re-check inside the txn. We map + // collisions back to per-field keys so the wizard can highlight + // the offending input precisely. + const dup = await client.query<{ + id: number; + username: string; + email: string; + }>( + `SELECT id, username, email FROM users WHERE username = $1 OR email = $2`, [input.admin.username, input.admin.email], ); if ((dup.rowCount ?? 0) > 0) { await client.query("ROLLBACK"); + const errors: ValidationErrors = {}; + for (const row of dup.rows) { + if (row.username === input.admin.username) + errors["admin.username"] = "username_taken"; + if (row.email === input.admin.email) + errors["admin.email"] = "email_taken"; + } return { ok: false, status: 400, error: "validation_failed", - errors: { "admin.username": "taken_or_email_taken" }, + errors, }; }