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';2
const crypto=require('node:crypto');const readline=require('node:readline');const {Writable}=require('node:stream');3
const {readConfig}=require('./config');const {openDatabase,one,run}=require('./database');4
const {passwordHash,base32Encode,totp,verifyTotp,encrypt}=require('./security');5
async 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 does8
// NOT reliably hide typed passwords and can cause an apparent hang. Instead,9
// suppress *all* readline output while collecting a secret. Node's readline10
// 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();}44
}45
async 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();}50
}51
if(require.main===module)main().catch(e=>{console.error('Setup error:',e.message);process.exitCode=1;});52
module.exports={createAdmin};