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.
46 lines
1.5 KiB
TypeScript
46 lines
1.5 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { generateRegistrationOptions } from "@simplewebauthn/server";
|
|
import type { AuthenticatorTransportFuture } from "@simplewebauthn/server";
|
|
import { query } from "@/lib/db";
|
|
import { requireSession, UnauthorizedError } from "@/lib/auth/session";
|
|
import { rpID, rpName } from "@/lib/auth/webauthn-config";
|
|
import { storeChallenge } from "@/lib/auth/webauthn-challenges";
|
|
|
|
export async function POST() {
|
|
try {
|
|
const session = await requireSession();
|
|
|
|
const existing = await query<{
|
|
credential_id: string;
|
|
transports: AuthenticatorTransportFuture[];
|
|
}>("select credential_id, transports from passkey_credentials where user_id = $1", [
|
|
session.user.id,
|
|
]);
|
|
|
|
const options = await generateRegistrationOptions({
|
|
rpName: rpName(),
|
|
rpID: rpID(),
|
|
userID: new TextEncoder().encode(session.user.id),
|
|
userName: session.user.email,
|
|
userDisplayName: session.user.full_name || session.user.email,
|
|
attestationType: "none",
|
|
excludeCredentials: existing.map((c) => ({
|
|
id: c.credential_id,
|
|
transports: c.transports,
|
|
})),
|
|
authenticatorSelection: {
|
|
residentKey: "preferred",
|
|
userVerification: "preferred",
|
|
},
|
|
});
|
|
|
|
await storeChallenge(options.challenge, "registration", session.user.id);
|
|
return NextResponse.json(options);
|
|
} catch (err) {
|
|
if (err instanceof UnauthorizedError) {
|
|
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
|
|
}
|
|
throw err;
|
|
}
|
|
}
|