Skip to content

Repository files, collaboration, releases and project activity.

archforge-external-status / src/cli.jsmain
▤ src/cli.js3445 bytes
53 lines · Select a line number for a permalink
1'use strict';
2const crypto=require('node:crypto');const readline=require('node:readline');const {Writable}=require('node:stream');
3const {readConfig}=require('./config');const {openDatabase,one,run}=require('./database');
4const {passwordHash,base32Encode,totp,verifyTotp,encrypt}=require('./security');
5async function createAdmin(db,cfg){if(one(db,'SELECT id FROM admin WHERE id=1')){console.log('An admin already exists. Existing credentials were not changed.');return;}
6 if(!process.stdin.isTTY || !process.stdout.isTTY)throw Error('Create admin from an interactive terminal (TTY)');
7 // readline owns the TTY's raw mode; calling `stty -echo` behind its back does
8 // NOT reliably hide typed passwords and can cause an apparent hang. Instead,
9 // suppress *all* readline output while collecting a secret. Node's readline
10 // keeps the terminal in raw mode, so the OS itself does not echo input either.
11 let hiding=false;
12 const output=new Writable({write(chunk,encoding,done){
13 if(!hiding)process.stdout.write(chunk);
14 done();
15 }});
16 const rl=readline.createInterface({input:process.stdin,output,terminal:true});
17 const ask=q=>new Promise((resolve,reject)=>{
18 const closed=()=>reject(new Error('Terminal input closed before setup completed'));
19 rl.once('close',closed);
20 rl.question(q,answer=>{rl.off('close',closed);resolve(answer);});
21 });
22 const hidden=async(q)=>{
23 process.stdout.write(q);
24 hiding=true;
25 try{return await ask('');}
26 finally{hiding=false;process.stdout.write('\n');}
27 };
28 rl.on('SIGINT',()=>rl.close());
29 try{
30 let name=(await ask('Admin username (3-40 letters, digits, _ or -): ')).trim();if(!/^[a-zA-Z0-9_-]{3,40}$/.test(name))throw Error('Invalid admin username');
31 let pass=await hidden('New admin password (minimum 12 characters): ');
32 if(pass.length<12)throw Error('Password must be at least 12 characters');
33 let repeat=await hidden('Confirm password: ');if(pass!==repeat)throw Error('Passwords do not match');
34 const secret=base32Encode(crypto.randomBytes(20));
35 console.log('\nIMPORTANT: Add the following TOTP secret to your authenticator app (Aegis, 2FAS, etc).');
36 console.log('TOTP SECRET:',secret);
37 console.log('Authenticator URI:',`otpauth://totp/ArchForge%20External%20Status:${encodeURIComponent(name)}?secret=${secret}&issuer=ArchForge%20External%20Status&digits=6&period=30`);
38 console.log('This secret is shown only during setup. Keep it private.');
39 const code=(await ask('Enter the current 6-digit code to confirm authenticator setup: ')).trim();
40 if(!verifyTotp(secret,code))throw Error('Invalid authenticator code. Run setup again.');
41 run(db,'INSERT INTO admin(id,username,password_hash,totp_encrypted) VALUES(1,?,?,?)',name,passwordHash(pass),encrypt(cfg.key,secret));
42 console.log('Admin created successfully. Two-factor authentication is required.');
43 }finally{rl.close();}
45async function main(){const cfg=readConfig(),db=openDatabase(cfg.dbPath);
46 try{if(process.argv[2]==='create-admin')await createAdmin(db,cfg);
47 else if(process.argv[2]==='verify-database'){console.log('Database ready:',one(db,'PRAGMA integrity_check').integrity_check);}
48 else{console.log('Usage: node src/cli.js create-admin | verify-database');process.exitCode=2;}}
49 finally{db.close();}
51if(require.main===module)main().catch(e=>{console.error('Setup error:',e.message);process.exitCode=1;});
52module.exports={createAdmin};