Der Admin-Benutzer wird immer in der Datenbank angelegt, bevor versucht wird, die generierten Zugangsdaten in ZUGANGSDATEN.md zu schreiben. Schlug dieser Schreibvorgang fehl (z. B. EACCES, weil die per Bind-Mount eingebundene Host-Datei root gehört und der App-Container als nicht-root läuft), flog bisher eine ungefangene Exception – das Skript brach ab, BEVOR das Passwort irgendwo protokolliert wurde. Der Admin existierte dann in der DB, aber niemand kannte sein Passwort mehr. appendFileSync() läuft jetzt in einem try/catch; schlägt der Dateizugriff fehl, werden E-Mail und Passwort stattdessen direkt ins Log geschrieben, und das Skript beendet sich weiterhin mit Exitcode 0 (die eigentlich kritische Operation – der DB-Insert – ist ja bereits erfolgreich). Lokal verifiziert: Skript bricht bei einer nicht beschreibbaren ZUGANGSDATEN.md nicht mehr ab, Zugangsdaten landen im Log, zweiter Lauf bleibt idempotent. DEPLOYMENT.md: Schritt 2 ergänzt um "chmod 666 ZUGANGSDATEN.md" (der eigentliche Auslöser), neue Fehlerbehebung-Einträge für ECONNREFUSED-Startreihenfolge und diesen EACCES-Fall inkl. Recovery-Anleitung für einen bereits verwaisten Admin-Account.
100 lines
3.5 KiB
JavaScript
100 lines
3.5 KiB
JavaScript
#!/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);
|
||
});
|