BillTracker/setup/firstRun.js

194 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, is_default_admin)
VALUES (?, ?, ?, 0, 0, ?)
`).run(username, hash, role, role === 'admin' ? 1 : 0);
}
async function runFromEnv(db) {
const adminUser = process.env.INIT_ADMIN_USER;
const adminPass = process.env.INIT_ADMIN_PASS;
const regularUser = process.env.INIT_REGULAR_USER;
const regularPass = process.env.INIT_REGULAR_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 (regularUser && !regularPass) errors.push('INIT_REGULAR_PASS required when INIT_REGULAR_USER is set');
if (regularPass && !regularUser) errors.push('INIT_REGULAR_USER required when INIT_REGULAR_PASS is set');
if (regularUser && regularUser.length < 3) errors.push('INIT_REGULAR_USER must be at least 3 characters');
if (regularPass && regularPass.length < 8) errors.push('INIT_REGULAR_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('Optionally set: INIT_REGULAR_USER and INIT_REGULAR_PASS for a non-admin test user');
console.error('Then open the web UI to create your first user account.\n');
process.exit(1);
}
const existingAdmin = db.prepare('SELECT id FROM users WHERE role = ? AND username = ?').get('admin', adminUser);
const adminHash = await bcrypt.hash(adminPass, 12);
if (existingAdmin) {
// Update existing admin's password
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(adminHash, existingAdmin.id);
console.log(`[first-run] Admin password updated for "${adminUser}".`);
} else {
// Create new admin user
db.prepare(`
INSERT INTO users (username, password_hash, role, first_login, must_change_password, is_default_admin)
VALUES (?, ?, ?, 0, 0, 1)
`).run(adminUser, adminHash, 'admin');
console.log(`[first-run] Admin "${adminUser}" created.`);
}
// Handle regular user creation if specified
if (regularUser && regularPass) {
const existingRegular = db.prepare('SELECT id FROM users WHERE role = ? AND username = ?').get('user', regularUser);
const regularHash = await bcrypt.hash(regularPass, 12);
if (existingRegular) {
// Update existing regular user's password
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(regularHash, existingRegular.id);
console.log(`[first-run] Regular user password updated for "${regularUser}".`);
} else {
// Create new regular user
db.prepare(`
INSERT INTO users (username, password_hash, role, first_login, must_change_password, is_default_admin)
VALUES (?, ?, ?, 0, 0, 0)
`).run(regularUser, regularHash, 'user');
console.log(`[first-run] Regular user "${regularUser}" created.`);
}
}
console.log('[first-run] You can now log in with these credentials.');
}
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 };