"Mein Konto": Profil, Passwort setzen oder wechseln, Passkeys anlegen, umbenennen und entfernen, aktive Sitzungen einsehen und ueberall abmelden. Der letzte Passkey laesst sich nicht entfernen, solange kein Passwort gesetzt ist - sonst waere das Konto ausgesperrt. Admin-Einstellungen: Betriebsart umschalten, Selbstregistrierung freigeben, Konten anlegen, bearbeiten, deaktivieren und loeschen. Die Instanzwerte aus der .env werden angezeigt, damit sichtbar ist, warum Passkeys gegebenenfalls nicht verfuegbar sind. Schutzregeln: - Der Wechsel in den Einzelbetrieb wird abgelehnt, solange mehr als ein Konto existiert. - Der letzte aktive Administrator kann weder herabgestuft noch deaktiviert oder geloescht werden. - Das eigene Konto laesst sich nicht loeschen oder deaktivieren. - Deaktivieren oder ein neues Passwort beenden sofort alle Sitzungen des Kontos. Administratoren verwalten Konten, keine Inhalte: fremde Projekte sehen auch sie nicht. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
import { NextResponse } from "next/server";
|
||
import { requireUserApi } from "@/lib/auth/guard";
|
||
import { prisma } from "@/lib/db";
|
||
import { jsonError, readJsonBody } from "@/lib/security/request";
|
||
import { formatZodError, passkeyNameSchema } from "@/lib/validation";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
type Params = { params: Promise<{ id: string }> };
|
||
|
||
/** Umbenennen – nur der eigene Passkey. */
|
||
export async function PATCH(request: Request, { params }: Params) {
|
||
const auth = await requireUserApi();
|
||
if (!auth.ok) return auth.response;
|
||
|
||
const { id } = await params;
|
||
const body = await readJsonBody(request);
|
||
if (!body.ok) return body.response;
|
||
|
||
const parsed = passkeyNameSchema.safeParse(body.data);
|
||
if (!parsed.success) {
|
||
return NextResponse.json(
|
||
{ error: "Validierung fehlgeschlagen", fields: formatZodError(parsed.error) },
|
||
{ status: 422 },
|
||
);
|
||
}
|
||
|
||
// Der Besitzvergleich steht in der where-Klausel, damit fremde Passkeys
|
||
// gar nicht erst getroffen werden.
|
||
const result = await prisma.credential.updateMany({
|
||
where: { id, userId: auth.user.id },
|
||
data: { name: parsed.data.name ?? null },
|
||
});
|
||
|
||
if (result.count === 0) return jsonError("Passkey nicht gefunden.", 404);
|
||
return NextResponse.json({ ok: true });
|
||
}
|
||
|
||
export async function DELETE(_request: Request, { params }: Params) {
|
||
const auth = await requireUserApi();
|
||
if (!auth.ok) return auth.response;
|
||
|
||
const { id } = await params;
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: auth.user.id },
|
||
select: { passwordHash: true, _count: { select: { credentials: true } } },
|
||
});
|
||
if (!user) return jsonError("Konto nicht gefunden.", 404);
|
||
|
||
// Nicht das letzte Anmeldemittel entfernen.
|
||
if (!user.passwordHash && user._count.credentials <= 1) {
|
||
return jsonError(
|
||
"Das ist der letzte Passkey und es ist kein Passwort gesetzt. " +
|
||
"Bitte zuerst ein Passwort vergeben.",
|
||
409,
|
||
);
|
||
}
|
||
|
||
const result = await prisma.credential.deleteMany({ where: { id, userId: auth.user.id } });
|
||
if (result.count === 0) return jsonError("Passkey nicht gefunden.", 404);
|
||
|
||
return NextResponse.json({ ok: true });
|
||
}
|