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:
2026-07-18 10:42:21 +02:00
parent 4c5376a8c7
commit 28c9ec15c8
28 changed files with 1618 additions and 153 deletions
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env node
// Wendet alle noch nicht angewendeten Dateien aus db/migrations/*.sql auf
// die per DATABASE_URL erreichbare Postgres-Datenbank an. Idempotent –
// kann bei jedem App-Start gefahrlos erneut laufen (Docker-Entrypoint,
// npm predev/prestart).
import { readdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import pg from "pg";
const migrationsDir = join(dirname(fileURLToPath(import.meta.url)), "migrations");
async function main() {
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
try {
await client.query(`
create table if not exists schema_migrations (
filename text primary key,
applied_at timestamptz not null default now()
)
`);
const { rows } = await client.query("select filename from schema_migrations");
const applied = new Set(rows.map((r) => r.filename));
const files = readdirSync(migrationsDir)
.filter((f) => f.endsWith(".sql"))
.sort();
for (const file of files) {
if (applied.has(file)) continue;
const sql = readFileSync(join(migrationsDir, file), "utf8");
console.log(`db:migrate – wende ${file} an…`);
await client.query("begin");
try {
await client.query(sql);
await client.query("insert into schema_migrations (filename) values ($1)", [file]);
await client.query("commit");
} catch (err) {
await client.query("rollback");
throw new Error(`Migration ${file} fehlgeschlagen: ${err.message}`, { cause: err });
}
}
console.log("db:migrate – Datenbank ist aktuell.");
} finally {
await client.end();
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});