Stand vor Einführung des Nacht-Agenten

This commit is contained in:
Kundencenter 2026-09-27 00:51:32 +02:00
commit 4763548bfb
168 changed files with 12726 additions and 0 deletions

56
apps/api/src/cli/index.ts Normal file
View file

@ -0,0 +1,56 @@
import { randomUUID } from 'node:crypto';
import { hash } from '@node-rs/argon2';
import '../core/config.js';
import { pool, one, run } from '../core/db.js';
import { audit } from '../core/audit.js';
const [cmd, ...args] = process.argv.slice(2);
const opt = (n: string) => args.find((a) => a.startsWith(`--${n}=`))?.slice(n.length + 3);
if (cmd === 'create-superadmin') {
const email = opt('email')?.toLowerCase(), name = opt('name') ?? 'Superadmin', pw = opt('password');
if (!email || !pw || pw.length < 12) { console.error('Nutzung: create-superadmin --email=… --name=… --password=<min. 12 Zeichen>'); process.exit(2); }
if (await one('SELECT 1 AS x FROM users WHERE email = ?', [email])) { console.error('E-Mail existiert bereits'); process.exit(1); }
const id = randomUUID();
await run("INSERT INTO users (id, email, name, password_hash, kind, staff_role, status, email_verified_at) VALUES (?,?,?,?,'staff','superadmin','active',UTC_TIMESTAMP(3))", [id, email, name, await hash(pw, { memoryCost: 19456, timeCost: 2, parallelism: 1 })]);
await audit({ actorType: 'system', action: 'user.create', resourceType: 'user', resourceId: id, after: { email, kind: 'staff', staffRole: 'superadmin', via: 'cli' } });
console.log('Superadmin angelegt:', email, '(2FA muss beim ersten Login eingerichtet werden)');
} else if (cmd === 'connector-secrets') {
// Zugangsdaten einer Verbindung aus einer geschützten Datei setzen (Werte erscheinen nie in Argumenten/Logs)
// Nutzung: connector-secrets --name=Licensing --file=/pfad/datei.txt (Zeilen SCHLÜSSEL=Wert; LICENSING_API_USER→username, LICENSING_API_PASSWORD→password)
const { readFileSync } = await import('node:fs');
const { encryptSecrets } = await import('@kc/connectors');
const { decrypt } = await import('../core/crypto.js');
const { enqueue } = await import('../core/jobs.js');
const name = opt('name'), file = opt('file');
if (!name || !file) { console.error('Nutzung: connector-secrets --name=<Verbindung> --file=<Datei>'); process.exit(2); }
const map: Record<string, string> = { LICENSING_API_USER: 'username', LICENSING_API_PASSWORD: 'password' };
const vals: Record<string, string> = {};
for (const line of readFileSync(file, 'utf8').split('\n')) { const m = /^([A-Z_]+)=(.*)$/.exec(line.trim()); if (m && map[m[1]!]) vals[map[m[1]!]!] = m[2]!; }
const inst = await one('SELECT id, connector_key, secrets_enc FROM connector_instances WHERE name = ?', [name]);
if (!inst) { console.error('Verbindung nicht gefunden'); process.exit(1); }
const old = inst.secrets_enc ? JSON.parse(decrypt(inst.secrets_enc)) : {};
await run('UPDATE connector_instances SET secrets_enc = ? WHERE id = ?', [encryptSecrets({ ...old, ...vals }), inst.id]);
await enqueue('connector.sync', { instanceId: inst.id }, { idempotencyKey: `sync:${inst.id}:secrets:${Date.now()}` });
await audit({ actorType: 'system', action: 'connector.update', resourceType: 'connector', resourceId: inst.id, connector: inst.connector_key, after: { secrets: Object.keys(vals), via: 'cli' } });
console.log('Zugangsdaten gesetzt für', name, '- Felder:', Object.keys(vals).join(', '), '- Abgleich eingeplant');
} else if (cmd === 'import-domains') {
// Domain-Preisliste (Einkauf) aus einer Textdatei einlesen: import-domains --file=/pfad/liste.txt
const { readFileSync } = await import('node:fs'); const { parsePriceList } = await import('../modules/domains/logic.js');
const file = opt('file'); if (!file) { console.error('Nutzung: import-domains --file=<Datei>'); process.exit(2); }
const { rows, skipped } = parsePriceList(readFileSync(file, 'utf8'));
for (const r of rows) await run('INSERT INTO domain_tlds (tld, term_months, cost1_cents, cost2_cents, cost3_cents, cost4_cents, setup_cents) VALUES (?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE term_months=VALUES(term_months), cost1_cents=VALUES(cost1_cents), cost2_cents=VALUES(cost2_cents), cost3_cents=VALUES(cost3_cents), cost4_cents=VALUES(cost4_cents), setup_cents=VALUES(setup_cents)', [r.tld, r.termMonths, ...r.costs, r.setupCents]);
console.log(`${rows.length} Endungen importiert, ${skipped.length} Zeilen übersprungen`, skipped);
} else if (cmd === 'backup') {
// backup run | restore-test [datei] | status
const { loadBackupConfig, runBackup, runRestoreTest, readStatus } = await import('../ops/backup.js');
const cfg = loadBackupConfig(); const sub = args[0];
if (sub === 'run') { const r = await runBackup(cfg); console.log(JSON.stringify({ ok: r.ok, file: r.file, sizeBytes: r.sizeBytes, targets: r.targets, error: r.error }, null, 2)); process.exitCode = r.ok ? 0 : 1; }
else if (sub === 'restore-test') { const r = await runRestoreTest(cfg, args[1]); console.log(JSON.stringify(r, null, 2)); process.exitCode = r.ok ? 0 : 1; }
else if (sub === 'status') console.log(JSON.stringify(await readStatus(cfg), null, 2));
else { console.error('Nutzung: backup run | restore-test [datei] | status'); process.exitCode = 2; }
} else {
console.error('Befehle: create-superadmin, connector-secrets, import-domains, backup');
process.exit(2);
}
await pool.end();