- /setup fuehrt beim ersten Aufruf durch das Anlegen des Administratorkontos und die Wahl der Betriebsart. Der Endpunkt funktioniert nur, solange es keinen Benutzer gibt; danach kann sich niemand mehr nachtraeglich ein Adminkonto erschleichen. Vorhandene Projekte ohne Besitzer werden dabei uebernommen und im Formular vorher angekuendigt. - /login mit Passwort oder Passkey. Der Passkey-Knopf entfaellt, wenn APP_URL keinen sicheren Kontext ergibt - mit Hinweis, woran es liegt. Das Ziel nach der Anmeldung wird auf relative Pfade begrenzt, sonst waere der weiter-Parameter ein offener Redirect. - /register nur im Mehrbenutzerbetrieb und nur, wenn ein Administrator die Selbstregistrierung freigegeben hat. - Kopfzeile mit Benutzermenue: eigenes Konto, Admin-Einstellungen, Abmelden. Ohne Anmeldung bleibt sie auf das Logo reduziert. - Layout liest Titel und Namen ueber generateMetadata, damit APP_NAME zur Laufzeit greift statt beim Build eingefroren zu werden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
48 lines
1.5 KiB
TypeScript
48 lines
1.5 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import type { RegistrationResponseJSON } from "@simplewebauthn/server";
|
|
import { requireUserApi } from "@/lib/auth/guard";
|
|
import { WebAuthnUnavailableError, confirmRegistration } from "@/lib/auth/webauthn";
|
|
import { jsonError, readJsonBody } from "@/lib/security/request";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
interface Payload {
|
|
response?: RegistrationResponseJSON;
|
|
name?: string | null;
|
|
}
|
|
|
|
/** Nimmt die Antwort des Authenticators entgegen und speichert den Passkey. */
|
|
export async function POST(request: Request) {
|
|
const auth = await requireUserApi();
|
|
if (!auth.ok) return auth.response;
|
|
|
|
const body = await readJsonBody<Payload>(request);
|
|
if (!body.ok) return body.response;
|
|
|
|
const attestation = body.data?.response;
|
|
if (!attestation || typeof attestation !== "object") {
|
|
return jsonError("Die Antwort des Authenticators fehlt.", 400);
|
|
}
|
|
|
|
try {
|
|
const credential = await confirmRegistration(
|
|
auth.user.id,
|
|
attestation,
|
|
typeof body.data?.name === "string" ? body.data.name : null,
|
|
);
|
|
return NextResponse.json({
|
|
ok: true,
|
|
credential: {
|
|
id: credential.id,
|
|
name: credential.name,
|
|
deviceType: credential.deviceType,
|
|
backedUp: credential.backedUp,
|
|
createdAt: credential.createdAt,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof WebAuthnUnavailableError) return jsonError(error.message, 501);
|
|
return jsonError(error instanceof Error ? error.message : String(error), 400);
|
|
}
|
|
}
|