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,36 @@
|
||||
import "server-only";
|
||||
import type { PoolClient, QueryResultRow } from "pg";
|
||||
import { pool } from "./pool";
|
||||
|
||||
export async function query<T extends QueryResultRow = QueryResultRow>(
|
||||
text: string,
|
||||
params?: unknown[]
|
||||
): Promise<T[]> {
|
||||
const result = await pool.query<T>(text, params);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
export async function queryOne<T extends QueryResultRow = QueryResultRow>(
|
||||
text: string,
|
||||
params?: unknown[]
|
||||
): Promise<T | null> {
|
||||
const rows = await query<T>(text, params);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export async function withTransaction<T>(
|
||||
fn: (client: PoolClient) => Promise<T>
|
||||
): Promise<T> {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("begin");
|
||||
const result = await fn(client);
|
||||
await client.query("commit");
|
||||
return result;
|
||||
} catch (err) {
|
||||
await client.query("rollback");
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import "server-only";
|
||||
import { Pool } from "pg";
|
||||
|
||||
// Next.js' Dev-Server lädt Module bei Hot-Reloads neu; ohne den
|
||||
// globalThis-Cache würde bei jedem Speichern ein neuer Pool (und damit
|
||||
// neue offene Verbindungen) entstehen.
|
||||
declare global {
|
||||
var __pgPool: Pool | undefined;
|
||||
}
|
||||
|
||||
export const pool =
|
||||
globalThis.__pgPool ??
|
||||
new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalThis.__pgPool = pool;
|
||||
}
|
||||
Reference in New Issue
Block a user