mirror of
https://github.com/guyg1474/hcrb.git
synced 2026-10-15 23:13:28 +02:00
- Gemeinsame Zugänge mit Rollen: Admin darf alles, Mitglied nutzt Kalender, Fahrtplaner, Treffpunkte und bearbeitet Mitglieder (kein Hinzufügen, Löschen, Import, keine Patches, keine Statistik-Korrektur) - Server prüft die Rechte (403), Konten ohne Rolle gelten als Admin - npm run user: add <name> [admin|mitglied], role <name> <rolle> - Neue öffentliche Seite Kooperationen mit Titelbild, Anführer, Social-Media-Links und Beschreibung, Pflege durch Admin - Kopfzeile bleibt bei mittlerer Breite einzeilig - Texte auf Wunsch gekürzt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
4.6 KiB
JavaScript
111 lines
4.6 KiB
JavaScript
// 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 <name> [admin|mitglied] Benutzer anlegen (Standard: mitglied, Passwort wird abgefragt)
|
||
// npm run user -- role <name> <admin|mitglied> Rolle ändern (meldet alle Sitzungen des Benutzers ab)
|
||
// npm run user -- passwd <name> Passwort ändern (meldet alle Sitzungen des Benutzers ab)
|
||
// npm run user -- remove <name> 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 <name> [admin|mitglied] | role <name> <admin|mitglied> | passwd <name> | remove <name>');
|
||
console.log(`Benutzerdatei: ${USERS_FILE}`);
|
||
process.exitCode = cmd ? 1 : 0;
|
||
}
|
||
} catch (err) {
|
||
console.error(err.message);
|
||
process.exitCode = 1;
|
||
}
|