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.
58 lines
1.8 KiB
JavaScript
58 lines
1.8 KiB
JavaScript
#!/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);
|
||
});
|