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.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user