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='); 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= --file='); process.exit(2); } const map: Record = { LICENSING_API_USER: 'username', LICENSING_API_PASSWORD: 'password' }; const vals: Record = {}; 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='); 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();