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=', '', '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 };