From a8dbf7bbb877aa3149c80559641322bc3d2e665c Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:11:28 +0200 Subject: [PATCH] fix(cli): preserve existing admin password during setup (#11522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated in a combined 10-PR batch worktree off release/v3.8.51 tip. Fixes #11494. - Focused test: tests/unit/cli-setup-command.test.ts — 10/10 pass - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity gates — all OK - Full-repo lint: 503 pre-existing problems confirmed identical on the pure release/v3.8.51 tip (isolated probe) — unrelated to this diff (bin/cli only) ⚠️ base-red inherited: #11449 Thanks for preventing INITIAL_PASSWORD from silently overwriting an operator's already-set admin password. --- bin/cli/commands/setup.mjs | 10 +++++----- tests/unit/cli-setup-command.test.ts | 29 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/bin/cli/commands/setup.mjs b/bin/cli/commands/setup.mjs index 5415cdd70b..2dd589006c 100644 --- a/bin/cli/commands/setup.mjs +++ b/bin/cli/commands/setup.mjs @@ -24,9 +24,9 @@ function wantsProviderSetup(opts) { return opts.addProvider || Boolean(opts.provider) || Boolean(opts.apiKey); } -async function resolvePassword(opts, prompt, nonInteractive) { - if (opts.password) return opts.password; - if (process.env.INITIAL_PASSWORD) return process.env.INITIAL_PASSWORD; +async function resolvePassword(opts, prompt, nonInteractive, settings) { + if (opts.password !== undefined) return opts.password; + if (!settings.password && process.env.INITIAL_PASSWORD) return process.env.INITIAL_PASSWORD; if (nonInteractive) return ""; const answer = await prompt.ask("Set an admin password now? [y/N]", "N"); @@ -41,9 +41,9 @@ async function resolvePassword(opts, prompt, nonInteractive) { } async function setupPassword(db, opts, prompt, nonInteractive) { - const password = await resolvePassword(opts, prompt, nonInteractive); + const settings = getSettings(db); + const password = await resolvePassword(opts, prompt, nonInteractive, settings); if (!password) { - const settings = getSettings(db); if (!settings.password) { updateSettings(db, { requireLogin: false }); } diff --git a/tests/unit/cli-setup-command.test.ts b/tests/unit/cli-setup-command.test.ts index ae0c805a65..0f433a59c1 100644 --- a/tests/unit/cli-setup-command.test.ts +++ b/tests/unit/cli-setup-command.test.ts @@ -250,3 +250,32 @@ test("setup command prioritizes an explicit --password flag over INITIAL_PASSWOR process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD; } }); + +test("setup command does not replace an existing password from INITIAL_PASSWORD", async () => { + const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD; + await withTempEnv(async (dataDir) => { + const { runSetupCommand } = await import("../../bin/cli/commands/setup.mjs"); + + await runSetupCommand({ nonInteractive: true, password: "existing-admin-secret" }); + + process.env.INITIAL_PASSWORD = "CHANGEME"; + const exitCode = await runSetupCommand({ nonInteractive: true }); + + assert.equal(exitCode, 0); + + const db = new Database(path.join(dataDir, "storage.sqlite")); + const passwordRow = db + .prepare("SELECT value FROM key_value WHERE namespace = 'settings' AND key = 'password'") + .get() as { value: string }; + db.close(); + + const storedHash = JSON.parse(passwordRow.value) as string; + assert.equal(await bcrypt.compare("existing-admin-secret", storedHash), true); + assert.equal(await bcrypt.compare("CHANGEME", storedHash), false); + }); + if (ORIGINAL_INITIAL_PASSWORD === undefined) { + delete process.env.INITIAL_PASSWORD; + } else { + process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD; + } +});