fix(cli): preserve existing admin password during setup (#11522)

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.
This commit is contained in:
Paco Cartones
2026-08-25 18:11:28 +02:00
committed by GitHub
parent a179ffed5b
commit a8dbf7bbb8
2 changed files with 34 additions and 5 deletions

View File

@@ -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 });
}

View File

@@ -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;
}
});