Files
electric-cherry/scripts/reset-admin.mjs
T

56 lines
3.1 KiB
JavaScript
Raw 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
// Local recovery only: requires filesystem/container access to the controller.
import Database from 'better-sqlite3';
import { emitKeypressEvents } from 'node:readline';
import { hashPassword } from '../src/lib/server/auth/passwords.js';
function secret(prompt) {
return new Promise((resolve, reject) => {
let value = '';
process.stdout.write(prompt);
emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.resume();
const cleanup = () => {
process.stdin.off('keypress', listener);
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdout.write('\n');
};
const listener = (text, key) => {
if (key?.ctrl && key.name === 'c') { cleanup(); reject(new Error('Cancelled.')); }
else if (key?.name === 'return' || key?.name === 'enter') { cleanup(); resolve(value); }
else if (key?.name === 'backspace') value = [...value].slice(0,-1).join('');
else if (text && !key?.ctrl && !key?.meta && !/[\x00-\x1f\x7f]/.test(text) && [...value].length < 128) value += text;
};
process.stdin.on('keypress',listener);
});
}
try {
const username = process.argv[2]?.toLowerCase();
if (process.argv.length !== 3 || !username || !/^[a-z0-9][a-z0-9_.-]{2,63}$/.test(username)) throw new Error('Usage: node scripts/reset-admin.mjs <administrator-username>');
if (!process.stdin.isTTY) throw new Error('Run this command in an interactive terminal. Passwords are never accepted as command arguments.');
const database = new Database(process.env.DB_PATH || './data/cherry.db',{fileMustExist:true});
database.pragma('busy_timeout=5000');
const user = database.prepare("SELECT id FROM auth_users WHERE username=? AND role='admin' AND disabled=0").get(username);
if (!user) throw new Error('An enabled administrator with that username was not found.');
const password = await secret('New administrator password (8–128 characters): ');
const confirmation = await secret('Repeat password: ');
if (password !== confirmation) throw new Error('Passwords did not match.');
const passwordHash = await hashPassword(password);
database.transaction(() => {
const changed = database.prepare("UPDATE auth_users SET password_hash=?,must_change_password=0,updated_at=? WHERE id=? AND role='admin' AND disabled=0")
.run(passwordHash,Date.now(),user.id);
if (changed.changes !== 1) throw new Error('Administrator changed during recovery. Try again.');
database.prepare('DELETE FROM auth_sessions WHERE user_id=?').run(user.id);
database.prepare('INSERT INTO auth_audit(actor_id,action,target_id,details,created_at) VALUES(NULL,?,?,?,?)')
.run('password.recovered',user.id,'{"source":"local-cli"}',Date.now());
}).immediate();
database.close();
console.log('Administrator password reset. Existing sessions have been revoked.');
} catch (cause) {
console.error(cause instanceof Error ? cause.message : 'Administrator recovery failed.');
process.exitCode=1;
}