feat: allow admin password overwrite during setup recovery

This commit is contained in:
Linus Rath
2026-05-16 18:21:48 +02:00
parent 307e6d5d34
commit 997bedc91b
2 changed files with 21 additions and 8 deletions
+8 -5
View File
@@ -58,13 +58,16 @@ export async function POST(request: NextRequest) {
} }
try { try {
// 1. Provision the admin account. Aborts cleanly if one already exists // 1. Provision the admin account. An admin.json file may already exist
// (defence in depth - should be impossible in bootstrap state). // from a previous ADMIN_PASSWORD env var or an aborted earlier wizard
const created = await setInitialAdminPassword(adminPassword); // 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) { if (!created) {
return NextResponse.json( return NextResponse.json(
{ error: 'Admin account already exists; cannot finish setup again' }, { error: 'Failed to write admin credentials' },
{ status: 409 }, { status: 500 },
); );
} }
+13 -3
View File
@@ -189,11 +189,21 @@ export async function changeAdminPassword(currentPassword: string, newPassword:
/** /**
* Set the admin password without verifying a current one. Used by the setup * 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<boolean> { export async function setInitialAdminPassword(
newPassword: string,
options: { allowOverwrite?: boolean } = {},
): Promise<boolean> {
const existing = await readConfigData(); const existing = await readConfigData();
if (existing) return false; if (existing && !options.allowOverwrite) return false;
const hash = await hashPassword(newPassword); const hash = await hashPassword(newPassword);
cachedConfig = { passwordHash: hash }; cachedConfig = { passwordHash: hash };
cachedState = freshState(); cachedState = freshState();