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