Skip to content

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-111 'use strict';
f1897d33c0archadmin2026-10-112 const crypto=require('node:crypto');const readline=require('node:readline');const {Writable}=require('node:stream');
e59b7f7cc6archadmin2026-10-113 const {readConfig}=require('./config');const {openDatabase,one,run}=require('./database');
e59b7f7cc6archadmin2026-10-114 const {passwordHash,base32Encode,totp,verifyTotp,encrypt}=require('./security');
e59b7f7cc6archadmin2026-10-115 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-116  if(!process.stdin.isTTY || !process.stdout.isTTY)throw Error('Create admin from an interactive terminal (TTY)');
f1897d33c0archadmin2026-10-117  // readline owns the TTY's raw mode; calling `stty -echo` behind its back does
f1897d33c0archadmin2026-10-118  // NOT reliably hide typed passwords and can cause an apparent hang. Instead,
f1897d33c0archadmin2026-10-119  // suppress *all* readline output while collecting a secret. Node's readline
f1897d33c0archadmin2026-10-1110  // keeps the terminal in raw mode, so the OS itself does not echo input either.
f1897d33c0archadmin2026-10-1111  let hiding=false;
f1897d33c0archadmin2026-10-1112  const output=new Writable({write(chunk,encoding,done){
f1897d33c0archadmin2026-10-1113  if(!hiding)process.stdout.write(chunk);
f1897d33c0archadmin2026-10-1114  done();
f1897d33c0archadmin2026-10-1115  }});
f1897d33c0archadmin2026-10-1116  const rl=readline.createInterface({input:process.stdin,output,terminal:true});
f1897d33c0archadmin2026-10-1117  const ask=q=>new Promise((resolve,reject)=>{
f1897d33c0archadmin2026-10-1118  const closed=()=>reject(new Error('Terminal input closed before setup completed'));
f1897d33c0archadmin2026-10-1119  rl.once('close',closed);
f1897d33c0archadmin2026-10-1120  rl.question(q,answer=>{rl.off('close',closed);resolve(answer);});
f1897d33c0archadmin2026-10-1121  });
e59b7f7cc6archadmin2026-10-1122  const hidden=async(q)=>{
f1897d33c0archadmin2026-10-1123  process.stdout.write(q);
f1897d33c0archadmin2026-10-1124  hiding=true;
f1897d33c0archadmin2026-10-1125  try{return await ask('');}
f1897d33c0archadmin2026-10-1126  finally{hiding=false;process.stdout.write('\n');}
e59b7f7cc6archadmin2026-10-1127  };
f1897d33c0archadmin2026-10-1128  rl.on('SIGINT',()=>rl.close());
e59b7f7cc6archadmin2026-10-1129  try{
e59b7f7cc6archadmin2026-10-1130  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-1131  let pass=await hidden('New admin password (minimum 12 characters): ');
e59b7f7cc6archadmin2026-10-1132  if(pass.length<12)throw Error('Password must be at least 12 characters');
e59b7f7cc6archadmin2026-10-1133  let repeat=await hidden('Confirm password: ');if(pass!==repeat)throw Error('Passwords do not match');
e59b7f7cc6archadmin2026-10-1134  const secret=base32Encode(crypto.randomBytes(20));
e59b7f7cc6archadmin2026-10-1135  console.log('\nIMPORTANT: Add the following TOTP secret to your authenticator app (Aegis, 2FAS, etc).');
e59b7f7cc6archadmin2026-10-1136  console.log('TOTP SECRET:',secret);
e59b7f7cc6archadmin2026-10-1137  console.log('Authenticator URI:',`otpauth://totp/ArchForge%20External%20Status:${encodeURIComponent(name)}?secret=${secret}&issuer=ArchForge%20External%20Status&digits=6&period=30`);
e59b7f7cc6archadmin2026-10-1138  console.log('This secret is shown only during setup. Keep it private.');
e59b7f7cc6archadmin2026-10-1139  const code=(await ask('Enter the current 6-digit code to confirm authenticator setup: ')).trim();
e59b7f7cc6archadmin2026-10-1140  if(!verifyTotp(secret,code))throw Error('Invalid authenticator code. Run setup again.');
e59b7f7cc6archadmin2026-10-1141  run(db,'INSERT INTO admin(id,username,password_hash,totp_encrypted) VALUES(1,?,?,?)',name,passwordHash(pass),encrypt(cfg.key,secret));
e59b7f7cc6archadmin2026-10-1142  console.log('Admin created successfully. Two-factor authentication is required.');
e59b7f7cc6archadmin2026-10-1143  }finally{rl.close();}
e59b7f7cc6archadmin2026-10-1144 }
e59b7f7cc6archadmin2026-10-1145 async function main(){const cfg=readConfig(),db=openDatabase(cfg.dbPath);
e59b7f7cc6archadmin2026-10-1146  try{if(process.argv[2]==='create-admin')await createAdmin(db,cfg);
e59b7f7cc6archadmin2026-10-1147  else if(process.argv[2]==='verify-database'){console.log('Database ready:',one(db,'PRAGMA integrity_check').integrity_check);}
e59b7f7cc6archadmin2026-10-1148  else{console.log('Usage: node src/cli.js create-admin | verify-database');process.exitCode=2;}}
e59b7f7cc6archadmin2026-10-1149  finally{db.close();}
e59b7f7cc6archadmin2026-10-1150 }
e59b7f7cc6archadmin2026-10-1151 if(require.main===module)main().catch(e=>{console.error('Setup error:',e.message);process.exitCode=1;});
e59b7f7cc6archadmin2026-10-1152 module.exports={createAdmin};