#!/usr/bin/env node // Legt beim allerersten Start (leere users-Tabelle) genau einen // Admin-Benutzer mit zufällig generiertem Passwort an und hängt die // Zugangsdaten an ZUGANGSDATEN.md an. Idempotent: bei bereits // vorhandenem Benutzer passiert nichts. // // Eigenständiges Skript statt Import aus lib/auth/password.ts, damit // dieses Vor-Build-Skript nicht an Next's Build-Output gekoppelt ist. // Die Hash-Logik ist bewusst identisch zur kanonischen Implementierung // in lib/auth/password.ts gehalten – Änderungen dort auch hier nachziehen. import { appendFileSync, existsSync } from "node:fs"; import { randomBytes, scryptSync } from "node:crypto"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import pg from "pg"; const N = 16384; const r = 8; const p = 1; const KEY_LEN = 64; const MAX_MEM = 32 * 1024 * 1024; function hashPassword(plain) { const salt = randomBytes(16); const hash = scryptSync(plain, salt, KEY_LEN, { N, r, p, maxmem: MAX_MEM }); return `scrypt$${N}$${r}$${p}$${salt.toString("hex")}$${hash.toString("hex")}`; } function defaultAdminEmail() { if (process.env.ADMIN_EMAIL) return process.env.ADMIN_EMAIL; try { const host = new URL(process.env.APP_URL ?? "http://localhost").hostname; return `admin@${host}`; } catch { return "admin@localhost"; } } async function main() { const client = new pg.Client({ connectionString: process.env.DATABASE_URL }); await client.connect(); try { const { rows } = await client.query("select count(*)::int as count from users"); if (rows[0].count > 0) { console.log("bootstrap-admin – Admin bereits vorhanden, überspringe."); return; } const email = defaultAdminEmail(); const password = randomBytes(18).toString("base64url"); const passwordHash = hashPassword(password); await client.query( `insert into users (email, password_hash, full_name, role, password_auth_enabled) values ($1, $2, '', 'admin', true)`, [email, passwordHash] ); // Der Admin-Datensatz steht ab hier in der Datenbank – das darf durch // ein Problem beim Ablegen der Zugangsdaten nicht mehr verloren gehen // (sonst gibt es einen Benutzer, dessen Passwort niemand kennt). // appendFileSync() also bewusst nicht mehr ungefangen werfen lassen, // sondern im Fehlerfall die Zugangsdaten direkt ins Log schreiben. const zugangsdatenPath = join( dirname(dirname(fileURLToPath(import.meta.url))), "ZUGANGSDATEN.md" ); const entry = `\n## Admin-Zugang (automatisch angelegt, ${new Date().toISOString().slice(0, 10)})\n\n- E-Mail: \`${email}\`\n- Passwort: \`${password}\`\n\nBitte nach dem ersten Login ändern.\n`; let wroteFile = false; try { if (existsSync(zugangsdatenPath)) { appendFileSync(zugangsdatenPath, entry); wroteFile = true; } } catch (err) { console.warn(`bootstrap-admin – Konnte ${zugangsdatenPath} nicht beschreiben: ${err.message}`); } if (wroteFile) { console.log(`bootstrap-admin – Admin angelegt: ${email} (Passwort siehe ZUGANGSDATEN.md)`); } else { console.warn( `bootstrap-admin – ${zugangsdatenPath} existiert nicht oder ist nicht beschreibbar. ` + `Zugangsdaten NUR HIER im Log, bitte sofort notieren:` ); console.log(`bootstrap-admin – Admin-E-Mail: ${email}`); console.log(`bootstrap-admin – Admin-Passwort: ${password}`); } } finally { await client.end(); } } main().catch((err) => { console.error(err); process.exit(1); });