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 };
|