Files
Speisekarte/app/api/account/route.ts
T
florian.wilde 28c9ec15c8 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.
2026-07-18 10:42:21 +02:00

51 lines
1.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextResponse } from "next/server";
import { query, queryOne } from "@/lib/db";
import { requireSession, UnauthorizedError } from "@/lib/auth/session";
import { isSameOrigin } from "@/lib/origin";
// Nimmt bewusst NUR full_name/password_auth_enabled aus dem Body an –
// ersetzt den früheren Postgres-Column-Grant, der self-service-Updates
// auf "role"/"email" verhindert hat. Ein generisches "update the whole
// body"-Endpoint würde diese Absicherung wieder aufheben.
export async function PATCH(request: Request) {
if (!isSameOrigin(request)) {
return NextResponse.json({ error: "Nicht erlaubt." }, { status: 403 });
}
try {
const session = await requireSession();
const body = (await request.json().catch(() => ({}))) as {
full_name?: string;
password_auth_enabled?: boolean;
};
if (body.password_auth_enabled === false) {
const passkeys = await queryOne<{ count: string }>(
"select count(*)::int as count from passkey_credentials where user_id = $1",
[session.user.id]
);
if (!passkeys || Number(passkeys.count) === 0) {
return NextResponse.json({ error: "needs_passkey" }, { status: 400 });
}
}
if (body.full_name !== undefined) {
const name = body.full_name.trim();
if (!name) return NextResponse.json({ error: "name_required" }, { status: 400 });
await query("update users set full_name = $1 where id = $2", [name, session.user.id]);
}
if (body.password_auth_enabled !== undefined) {
await query("update users set password_auth_enabled = $1 where id = $2", [
body.password_auth_enabled,
session.user.id,
]);
}
return NextResponse.json({ ok: true });
} catch (err) {
if (err instanceof UnauthorizedError) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}
throw err;
}
}