BillTracker/setup/firstRun.js

151 lines
4.4 KiB
JavaScript
Raw Normal View History

2026-05-03 19:51:57 -05:00
const readline = require('readline');
const bcrypt = require('bcryptjs');
function line(char = '─', len = 56) {
return char.repeat(len);
}
function prompt(question) {
return new Promise(resolve => {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question(question, answer => {
rl.close();
resolve(answer.trim());
});
});
}
function promptPassword(label) {
if (!process.stdin.isTTY) {
return prompt(label);
}
return new Promise(resolve => {
process.stdout.write(label);
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
let pw = '';
const handler = (ch) => {
if (ch === '\n' || ch === '\r' || ch === '') {
process.stdout.write('\n');
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdin.removeListener('data', handler);
resolve(pw);
} else if (ch === '') {
process.exit(0);
} else if (ch === '') {
if (pw.length > 0) {
pw = pw.slice(0, -1);
process.stdout.write('\b \b');
}
} else {
pw += ch;
process.stdout.write('*');
}
};
process.stdin.on('data', handler);
});
}
async function createUser(db, username, password, role) {
const hash = await bcrypt.hash(password, 12);
db.prepare(`
INSERT INTO users (username, password_hash, role, first_login, must_change_password)
VALUES (?, ?, ?, 0, 0)
`).run(username, hash, role);
}
async function runFromEnv(db) {
const adminUser = process.env.INIT_ADMIN_USER;
const adminPass = process.env.INIT_ADMIN_PASS;
const errors = [];
if (!adminUser || adminUser.length < 3) errors.push('INIT_ADMIN_USER must be at least 3 characters');
if (!adminPass || adminPass.length < 8) errors.push('INIT_ADMIN_PASS must be at least 8 characters');
if (errors.length) {
console.error('\n[first-run] Environment variable setup failed:');
errors.forEach(e => console.error(' ✗ ' + e));
console.error('\nSet both vars: INIT_ADMIN_USER and INIT_ADMIN_PASS');
console.error('Then open the web UI to create your first user account.\n');
process.exit(1);
}
await createUser(db, adminUser, adminPass, 'admin');
console.log(`[first-run] Admin "${adminUser}" created. Open the web UI to create your first user.`);
}
async function run(db) {
// Non-interactive Docker path: env vars provided
if (process.env.INIT_ADMIN_USER && process.env.INIT_ADMIN_PASS) {
return runFromEnv(db);
}
// No TTY and no env vars
if (!process.stdin.isTTY) {
console.error([
'',
'[first-run] No admin account found and no TTY available for interactive setup.',
'Set these environment variables to create the admin automatically:',
' INIT_ADMIN_USER=admin',
' INIT_ADMIN_PASS=<min 8 chars>',
'',
'Or run interactively: docker run -it ...',
'',
].join('\n'));
process.exit(1);
}
// Interactive terminal wizard (admin only)
console.log('\n' + line('═'));
console.log(' Bill Tracker — First Run Setup');
console.log(line('═'));
console.log(`
No accounts found. Create an admin account to get started.
About the admin account:
Can create user accounts via the web interface
Can reset user passwords
Cannot view, edit, or access any bills, payments,
or financial data ever
`);
console.log(line());
console.log(' Create Admin Account');
console.log(line());
console.log('');
let username, password, confirm;
while (true) {
username = await prompt(' Username: ');
if (username.length >= 3) break;
console.log(' Username must be at least 3 characters.\n');
}
while (true) {
password = await promptPassword(' Password: ');
if (password.length < 8) { console.log(' Password must be at least 8 characters.\n'); continue; }
confirm = await promptPassword(' Confirm: ');
if (password !== confirm) { console.log(' Passwords do not match.\n'); continue; }
break;
}
await createUser(db, username, password, 'admin');
console.log(`\n ✓ Admin account "${username}" created.\n`);
console.log(line('═'));
console.log(' Setup complete!');
console.log('');
console.log(' Open the app in your browser and log in as admin');
console.log(' to create your first user account.');
console.log(line('═'));
console.log('');
}
module.exports = { run };