Files
2026-07-13 13:39:12 +08:00

135 lines
4.5 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Password Reset CLI — T-38
*
* Usage:
* node bin/reset-password.mjs
* omniroute reset-password
*
* Non-interactive / scripted usage (piped stdin, e.g. CI or Docker):
* printf 'NewPass123\nNewPass123\n' | omniroute reset-password
* printf 'NewPass123' | omniroute reset-password --password-stdin
*
* Resets the admin password for OmniRoute.
* Prompts for a new password (interactive TTY) or reads it from stdin
* (non-TTY) and updates the database directly.
*
* @module bin/reset-password
*/
import { createInterface } from "node:readline";
import { resolveDataDir, resolveStoragePath } from "./cli/data-dir.mjs";
import { readManagementPasswordState, resetManagementPassword } from "./cli/sqlite.mjs";
// Resolve data directory — same logic as the server
const DATA_DIR = resolveDataDir();
const DB_PATH = resolveStoragePath(DATA_DIR);
const MIN_PASSWORD_LENGTH = 8;
/** Read the entire stdin stream as a UTF-8 string (used for non-TTY input). */
function readAllStdin() {
return new Promise((resolve) => {
let data = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
data += chunk;
});
process.stdin.on("end", () => resolve(data));
process.stdin.on("error", () => resolve(data));
// Resuming is implied by attaching a 'data' listener, but be explicit so a
// paused stream (some spawn setups) still flows to EOF.
process.stdin.resume();
});
}
/**
* Obtain the new password (and its confirmation).
*
* - `--password-stdin`: the ENTIRE stdin is the password, no confirmation.
* - non-TTY stdin (piped): read all of stdin once; first line is the password,
* second line — when present — is the confirmation, else the first line is
* reused (a single-line pipe means "no separate confirmation").
* - interactive TTY: two sequential prompts (unchanged behavior).
*
* The non-TTY path exists because two sequential `rl.question` promises never
* settle under a piped EOF — the second read blocks forever, so the reset was
* silently never applied (#6258).
*/
async function collectPassword() {
if (process.argv.includes("--password-stdin")) {
const raw = await readAllStdin();
const password = raw.replace(/[\r\n]+$/, "");
return { password, confirm: password };
}
if (!process.stdin.isTTY) {
const raw = await readAllStdin();
const lines = raw.split(/\r?\n/);
const password = lines[0] ?? "";
const confirm = lines[1] ? lines[1] : password;
return { password, confirm };
}
const rl = createInterface({ input: process.stdin, output: process.stdout });
try {
const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
const password = await ask("Enter new password (min 8 chars): ");
const confirm = await ask("Confirm new password: ");
return { password, confirm };
} finally {
rl.close();
}
}
console.log("\n🔑 OmniRoute — Password Reset\n");
async function main() {
// Check if database exists
const passwordState = await readManagementPasswordState(DB_PATH);
if (!passwordState.exists) {
console.error(`❌ Database not found at: ${DB_PATH}`);
console.error(` Make sure OmniRoute has been started at least once.`);
console.error(` Or set DATA_DIR env var to your data directory.\n`);
process.exit(1);
}
if (passwordState.hasPassword) {
console.log("️ A password is currently set.");
} else {
console.log("️ No password is currently set.");
}
const { password, confirm } = await collectPassword();
if (!password || password.length < MIN_PASSWORD_LENGTH) {
console.error(`\n❌ Password must be at least ${MIN_PASSWORD_LENGTH} characters.\n`);
process.exit(1);
}
if (password !== confirm) {
console.error("\n❌ Passwords do not match.\n");
process.exit(1);
}
await resetManagementPassword(password, DB_PATH);
console.log("\n✅ Password reset successfully!");
console.log(" Restart OmniRoute for changes to take effect.\n");
}
main()
.then(() => {
// Explicit exit(0) so a caller that imports this module (bin/omniroute.mjs
// routes `omniroute reset-password` here) terminates cleanly instead of
// hanging / exiting with code 13 on an unsettled wrapper await. On POSIX,
// console.log to a pipe is synchronous, so the success line is already
// flushed by the time we exit.
process.exit(0);
})
.catch((err) => {
console.error(`\n❌ Error: ${err.message}\n`);
process.exit(1);
});