import type { NextRequest } from "next/server"; import { z } from "zod"; import { getActor } from "@/lib/auth/actor"; import { describePasswordRules, hashPassword, MIN_PASSWORD_LENGTH, } from "@/lib/auth/password"; import { requirePermission } from "@/lib/auth/policy"; import { destroyAllSessions } from "@/lib/auth/session"; import { prisma } from "@/lib/db"; import { errorResponse, firstZodMessage, jsonError } from "@/lib/http"; import { QrRequestError } from "@/lib/qr/type-module"; export const dynamic = "force-dynamic"; type Context = { params: Promise<{ id: string }> }; const updateSchema = z.object({ name: z.string().trim().max(120).nullable().optional(), isActive: z.boolean().optional(), isInstanceAdmin: z.boolean().optional(), password: z .string() .min(MIN_PASSWORD_LENGTH, { error: `Das Passwort ist zu kurz. ${describePasswordRules()}`, }) .optional(), /** Sperre nach Fehlversuchen aufheben. */ unlock: z.boolean().optional(), }); /** * Verhindert, dass sich die Installation selbst aussperrt: es muss immer * mindestens ein aktiver Administrator übrig bleiben. */ async function assertNotLastAdmin(userId: string, action: string) { const user = await prisma.user.findUnique({ where: { id: userId }, select: { isInstanceAdmin: true, isActive: true }, }); if (!user?.isInstanceAdmin || !user.isActive) return; const others = await prisma.user.count({ where: { isInstanceAdmin: true, isActive: true, id: { not: userId } }, }); if (others === 0) { throw new QrRequestError( `Das ist der letzte aktive Administrator — ${action} würde die Installation unbedienbar machen. Lege zuerst einen weiteren Administrator an.`, ); } } export async function PATCH( request: NextRequest, context: Context, ): Promise { try { const actor = await getActor(); requirePermission(actor, "instance:manage"); const { id } = await context.params; const parsed = updateSchema.safeParse(await request.json()); if (!parsed.success) { return jsonError(firstZodMessage(parsed.error)); } const target = await prisma.user.findUnique({ where: { id } }); if (!target) { throw new QrRequestError("Dieser Benutzer existiert nicht (mehr).", 404); } const data: { name?: string | null; isActive?: boolean; isInstanceAdmin?: boolean; passwordHash?: string; failedLogins?: number; lockedUntil?: Date | null; } = {}; if (parsed.data.name !== undefined) { data.name = parsed.data.name || null; } if (parsed.data.isActive === false) { await assertNotLastAdmin(id, "das Deaktivieren"); data.isActive = false; } else if (parsed.data.isActive === true) { data.isActive = true; } if (parsed.data.isInstanceAdmin === false) { await assertNotLastAdmin(id, "der Entzug der Administratorrechte"); data.isInstanceAdmin = false; } else if (parsed.data.isInstanceAdmin === true) { data.isInstanceAdmin = true; } if (parsed.data.password) { data.passwordHash = await hashPassword(parsed.data.password); data.failedLogins = 0; data.lockedUntil = null; } if (parsed.data.unlock) { data.failedLogins = 0; data.lockedUntil = null; } if (Object.keys(data).length === 0) { return jsonError("Es wurden keine Änderungen übermittelt."); } const updated = await prisma.user.update({ where: { id }, data }); // Deaktivieren oder ein neues Passwort beenden laufende Sitzungen — // sonst bliebe der Betroffene angemeldet. if (data.isActive === false || data.passwordHash) { await destroyAllSessions(id); } return Response.json({ user: { id: updated.id, email: updated.email, isActive: updated.isActive, isInstanceAdmin: updated.isInstanceAdmin, }, }); } catch (error) { return errorResponse(error); } } export async function DELETE( _request: NextRequest, context: Context, ): Promise { try { const actor = await getActor(); requirePermission(actor, "instance:manage"); const { id } = await context.params; if (actor.kind === "user" && actor.user.id === id) { throw new QrRequestError( "Du kannst dein eigenes Konto nicht löschen.", ); } const target = await prisma.user.findUnique({ where: { id }, select: { id: true }, }); if (!target) { throw new QrRequestError("Dieser Benutzer existiert nicht (mehr).", 404); } await assertNotLastAdmin(id, "das Löschen"); // Die Codes des Benutzers bleiben bestehen; nur der Ersteller entfällt // (onDelete: SetNull). Gedruckte Codes dürfen nicht daran hängen, wer // sie einmal angelegt hat. await prisma.user.delete({ where: { id } }); return new Response(null, { status: 204 }); } catch (error) { return errorResponse(error); } }