// Creates (or with --force, replaces) the admin login file, outside the app, so the web // interface never has to run unsecured. No dependencies beyond the app's own; runs on // Node 24+ directly: // // onvif-dashboard admin # prompts for username and password // onvif-dashboard admin -- --username admin # prompts for password only // onvif-dashboard admin -- --force # replace an existing admin (resets password) // printf '%s\n' "$PW" | onvif-dashboard admin -- --username admin # non-interactive // // Writes ADMIN_AUTH_FILE if set, otherwise ./.data/admin.json, owner-only (0600). import { createInterface } from "node:readline/promises"; import { Writable } from "node:stream"; import { parseArgs } from "node:util"; import { adminFilePath, createAdminRecord, passwordSchema, readAdminFile, usernameSchema, writeAdminFile, } from "../dist/lib/admin-file.js"; const { values } = parseArgs({ options: { username: { type: "string" }, force: { type: "boolean", default: false }, help: { type: "boolean", short: "h", default: false }, }, }); if (values.help) { console.log("Usage: onvif-dashboard admin -- [--username NAME] [--force]"); process.exit(0); } const file = adminFilePath(); const interactive = process.stdin.isTTY === true; // Echo is switched off while a password is typed. let muted = false; const output = new Writable({ write(chunk, _encoding, done) { if (!muted) process.stdout.write(chunk); done(); }, }); const rl = createInterface({ input: process.stdin, output, terminal: interactive }); // Piped input can arrive before the questions are asked, so read it line by line. const lines = interactive ? null : rl[Symbol.asyncIterator](); async function ask(question, { secret = false } = {}) { if (lines) { const next = await lines.next(); return next.done ? "" : String(next.value); } if (!secret) return rl.question(question); process.stdout.write(question); muted = true; const answer = await rl.question(""); muted = false; process.stdout.write("\n"); return answer; } function fail(message) { console.error(`Error: ${message}`); rl.close(); process.exit(1); } async function main() { const existing = await readAdminFile(file).catch((err) => fail(err.message)); if (existing && !values.force) { fail(`an admin ("${existing.username}") already exists in ${file}. Re-run with --force to replace it.`); } const username = values.username ?? (await ask("Admin username: ")); const name = usernameSchema.safeParse(username); if (!name.success) fail(`username ${name.error.issues[0].message}`); const password = await ask("Password (12+ characters): ", { secret: true }); const strong = passwordSchema.safeParse(password); if (!strong.success) fail(`password: ${strong.error.issues[0].message}`); if (interactive && (await ask("Repeat password: ", { secret: true })) !== password) { fail("passwords don't match."); } rl.close(); await writeAdminFile(file, await createAdminRecord(username, password), { overwrite: values.force, }); console.log(`Admin "${username}" saved to ${file} (owner-only).`); if (existing) console.log("Existing sessions are signed out; log in again with the new password."); } await main();