Files
Speisekarte/scripts/bootstrap-admin.mjs
T
florian.wilde 28c9ec15c8 v1.5: Selbst gehostete Auth (Passwort + Passkeys), Postgres statt Supabase
Fundament: db/migrations/0001_init.sql (users/sessions/passkey_credentials/
webauthn_challenges + Inhaltstabellen ohne RLS), db/migrate.mjs
(idempotenter Migrationsrunner), lib/db (pg-Pool + query-Wrapper),
lib/auth/password.ts (Node-crypto.scrypt, keine neue Abhängigkeit),
lib/auth/session.ts (opake DB-Session-Tokens, Sliding-Refresh in proxy.ts).

Passkeys: @simplewebauthn/server + /browser ersetzen Supabase's
experimentelles WebAuthn-API (dafür gab es bisher kein eigenes
Credential-Schema); RP-Konfiguration kommt jetzt direkt aus APP_URL statt
über scripts/configure-passkeys.mjs und die Supabase-Management-API.

scripts/bootstrap-admin.mjs legt bei leerer users-Tabelle automatisch
einen Admin mit zufälligem Passwort an und hängt die Zugangsdaten an
ZUGANGSDATEN.md an (idempotent, via predev/prestart-Hooks aufgerufen).

Bugfix im Zuge des Umbaus: "mind. 1 Passkey nötig, um Passwort-Login zu
deaktivieren" wurde bisher nur clientseitig geprüft – jetzt zusätzlich
serverseitig in PATCH /api/account.

E-Mail-Änderung im eigenen Konto läuft jetzt direkt nach
Passwort-Bestätigung statt über eine Supabase-Bestätigungsmail (kein
SMTP-Server vorhanden).

Rest der Anwendung (Gerichte/Essenszeiten/Kennzeichnung/Vorschläge/
Benutzerverwaltung/Branding) nutzt noch den alten Supabase-Client –
folgt in weiteren Commits.
2026-07-18 10:42:21 +02:00

83 lines
2.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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]
);
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`;
if (existsSync(zugangsdatenPath)) {
appendFileSync(zugangsdatenPath, entry);
} else {
console.warn(
`bootstrap-admin – ${zugangsdatenPath} existiert nicht, Zugangsdaten werden nur geloggt:`
);
}
console.log(`bootstrap-admin – Admin angelegt: ${email} (Passwort siehe ZUGANGSDATEN.md)`);
} finally {
await client.end();
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});