// Benutzerverwaltung für den Login // // Die Konten sind gemeinsame Zugänge (z. B. "admin" und "hcrb" für alle Mitglieder), keine persönlichen. // // npm run user -- list alle Benutzer mit Rolle // npm run user -- add [admin|mitglied] Benutzer anlegen (Standard: mitglied, Passwort wird abgefragt) // npm run user -- role Rolle ändern (meldet alle Sitzungen des Benutzers ab) // npm run user -- passwd Passwort ändern (meldet alle Sitzungen des Benutzers ab) // npm run user -- remove Benutzer löschen import { createInterface } from 'node:readline'; import { hashPassword, readUsers, writeUsers, normalizeName, MIN_PASSWORD, USERS_FILE, ROLES, roleOf } from '../auth.js'; // Passwort verdeckt einlesen (bei umgeleiteter Eingabe zeilenweise) let pipedLines; function askPassword(prompt) { return new Promise((resolve, reject) => { const { stdin, stdout } = process; stdout.write(prompt); if (!stdin.isTTY) { pipedLines ??= createInterface({ input: stdin })[Symbol.asyncIterator](); pipedLines.next().then(({ value }) => { stdout.write('\n'); resolve(value ?? ''); }, reject); return; } let input = ''; stdin.setRawMode(true); stdin.setEncoding('utf8'); stdin.resume(); const onData = (chunk) => { for (const ch of chunk) { if (ch === '\r' || ch === '\n') { stdin.setRawMode(false); stdin.pause(); stdin.off('data', onData); stdout.write('\n'); return resolve(input); } if (ch === '\u0003') { // Strg+C stdin.setRawMode(false); stdout.write('\n'); return reject(new Error('Abgebrochen.')); } if (ch === '\u007f' || ch === '\b') input = input.slice(0, -1); else input += ch; } }; stdin.on('data', onData); }); } async function newPassword() { const pw = await askPassword('Neues Passwort: '); if (pw.length < MIN_PASSWORD) throw new Error(`Das Passwort muss mindestens ${MIN_PASSWORD} Zeichen haben.`); if (await askPassword('Passwort wiederholen: ') !== pw) throw new Error('Die Passwörter stimmen nicht überein.'); return hashPassword(pw); } const [cmd, rawName, rawRole] = process.argv.slice(2); const role = String(rawRole ?? '').trim().toLowerCase(); function checkRole(value) { if (!ROLES.includes(value)) throw new Error(`Rolle muss ${ROLES.join(' oder ')} sein.`); return value; } const name = normalizeName(rawName); const users = readUsers(); const user = users.find((u) => u.name === name); try { if (cmd === 'list') { console.log(users.length ? users.map((u) => `${u.name.padEnd(20)} ${roleOf(u)}${u.role ? '' : ' (noch ohne Rolle gespeichert)'}`).join('\n') : 'Noch keine Benutzer angelegt.'); } else if (cmd === 'add' && name) { if (!/^[a-z0-9._-]{2,32}$/.test(name)) { throw new Error('Benutzername: 2–32 Zeichen, nur a–z, 0–9, Punkt, Minus, Unterstrich.'); } if (user) throw new Error(`Benutzer „${name}“ gibt es schon.`); const newRole = checkRole(role || 'mitglied'); users.push({ name, role: newRole, hash: await newPassword(), changedAt: Date.now() }); writeUsers(users); console.log(`Benutzer „${name}“ als ${newRole} angelegt.`); } else if (cmd === 'role' && name && role) { if (!user) throw new Error(`Benutzer „${name}“ gibt es nicht.`); user.role = checkRole(role); if (user.role === 'mitglied' && !users.some((u) => roleOf(u) === 'admin')) { throw new Error('Das ist der letzte Admin. Lege zuerst einen anderen Admin an.'); } user.changedAt = Date.now(); writeUsers(users); console.log(`„${name}“ ist jetzt ${user.role}. Bestehende Anmeldungen sind abgemeldet.`); } else if (cmd === 'passwd' && name) { if (!user) throw new Error(`Benutzer „${name}“ gibt es nicht.`); user.hash = await newPassword(); user.changedAt = Date.now(); writeUsers(users); console.log(`Passwort von „${name}“ geändert. Bestehende Anmeldungen sind abgemeldet.`); } else if (cmd === 'remove' && name) { if (!user) throw new Error(`Benutzer „${name}“ gibt es nicht.`); writeUsers(users.filter((u) => u !== user)); console.log(`Benutzer „${name}“ gelöscht.`); } else { console.log('Benutzung: npm run user -- list | add [admin|mitglied] | role | passwd | remove '); console.log(`Benutzerdatei: ${USERS_FILE}`); process.exitCode = cmd ? 1 : 0; } } catch (err) { console.error(err.message); process.exitCode = 1; }