151 lines
4.4 KiB
JavaScript
151 lines
4.4 KiB
JavaScript
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 };
|