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.
83 lines
2.8 KiB
JavaScript
83 lines
2.8 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]
|
||
);
|
||
|
||
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);
|
||
});
|