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,50 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { query, queryOne } from "@/lib/db";
|
||||
import { verifyPassword } from "@/lib/auth/password";
|
||||
import { requireSession, UnauthorizedError } from "@/lib/auth/session";
|
||||
import { isSameOrigin } from "@/lib/origin";
|
||||
|
||||
// Ändert die E-Mail-Adresse direkt nach Passwort-Bestätigung – ohne
|
||||
// Bestätigungsmail, da kein SMTP-Server betrieben wird.
|
||||
export async function PATCH(request: Request) {
|
||||
if (!isSameOrigin(request)) {
|
||||
return NextResponse.json({ error: "Nicht erlaubt." }, { status: 403 });
|
||||
}
|
||||
try {
|
||||
const session = await requireSession();
|
||||
const { email, password } = (await request.json().catch(() => ({}))) as {
|
||||
email?: string;
|
||||
password?: string;
|
||||
};
|
||||
if (!email || !password) {
|
||||
return NextResponse.json({ error: "invalid_input" }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await queryOne<{ password_hash: string }>(
|
||||
"select password_hash from users where id = $1",
|
||||
[session.user.id]
|
||||
);
|
||||
if (!user || !verifyPassword(password, user.password_hash)) {
|
||||
return NextResponse.json({ error: "wrong_password" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
await query("update users set email = $1 where id = $2", [
|
||||
email.trim().toLowerCase(),
|
||||
session.user.id,
|
||||
]);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && /unique/i.test(err.message)) {
|
||||
return NextResponse.json({ error: "email_used" }, { status: 409 });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err) {
|
||||
if (err instanceof UnauthorizedError) {
|
||||
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user