diff --git a/app/api/setup/finish/route.ts b/app/api/setup/finish/route.ts index 4c0d11df..381cebbe 100644 --- a/app/api/setup/finish/route.ts +++ b/app/api/setup/finish/route.ts @@ -58,13 +58,16 @@ export async function POST(request: NextRequest) { } try { - // 1. Provision the admin account. Aborts cleanly if one already exists - // (defence in depth - should be impossible in bootstrap state). - const created = await setInitialAdminPassword(adminPassword); + // 1. Provision the admin account. An admin.json file may already exist + // from a previous ADMIN_PASSWORD env var or an aborted earlier wizard + // run while setupComplete is still false — accept the wizard's + // password as authoritative in that case. The finish route is gated + // by the bootstrap state + one-time setup token, so this is safe. + const created = await setInitialAdminPassword(adminPassword, { allowOverwrite: true }); if (!created) { return NextResponse.json( - { error: 'Admin account already exists; cannot finish setup again' }, - { status: 409 }, + { error: 'Failed to write admin credentials' }, + { status: 500 }, ); } diff --git a/lib/admin/password.ts b/lib/admin/password.ts index 955d81fb..4f4fd7d7 100644 --- a/lib/admin/password.ts +++ b/lib/admin/password.ts @@ -189,11 +189,21 @@ export async function changeAdminPassword(currentPassword: string, newPassword: /** * Set the admin password without verifying a current one. Used by the setup - * wizard during initial bootstrap. Refuses to overwrite an existing password. + * wizard during initial bootstrap. + * + * Refuses to overwrite an existing password unless `allowOverwrite` is true. + * The wizard's finish route passes `allowOverwrite: true` so a half-completed + * setup (admin.json left behind by an ADMIN_PASSWORD env var or an aborted + * earlier wizard run, while setupComplete is still false) can be recovered + * by simply running the wizard again. Safe because the finish route is + * already gated by the one-time setup token. */ -export async function setInitialAdminPassword(newPassword: string): Promise { +export async function setInitialAdminPassword( + newPassword: string, + options: { allowOverwrite?: boolean } = {}, +): Promise { const existing = await readConfigData(); - if (existing) return false; + if (existing && !options.allowOverwrite) return false; const hash = await hashPassword(newPassword); cachedConfig = { passwordHash: hash }; cachedState = freshState();