import type { NextRequest } from "next/server"; import { z } from "zod"; import { getActor } from "@/lib/auth/actor"; import { DEFAULT_GROUP_ID } from "@/lib/auth/groups"; import { requirePermission } from "@/lib/auth/policy"; 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().min(1).max(80).optional(), description: z.string().trim().max(300).nullable().optional(), /** * Vollständige Mitgliederliste. Wer fehlt, verliert die Mitgliedschaft — * so bleibt der Zustand nach dem Speichern eindeutig. */ members: z .array( z.object({ userId: z.string().trim().min(1), role: z.enum(["ADMIN", "EDITOR", "VIEWER"], { error: "Ungültige Rolle.", }), }), ) .optional(), }); export async function PATCH( request: NextRequest, context: Context, ): Promise { try { const actor = await getActor(); const { id } = await context.params; const group = await prisma.group.findUnique({ where: { id } }); if (!group) { throw new QrRequestError("Diese Gruppe existiert nicht (mehr).", 404); } const parsed = updateSchema.safeParse(await request.json()); if (!parsed.success) { return jsonError(firstZodMessage(parsed.error)); } // Name und Beschreibung sind Sache der Instanz-Verwaltung, Mitglieder // darf auch ein Administrator der Gruppe selbst pflegen. if (parsed.data.name !== undefined || parsed.data.description !== undefined) { requirePermission(actor, "instance:manage"); } if (parsed.data.members !== undefined) { requirePermission(actor, "group:manage", { groupId: id }); } if (parsed.data.name !== undefined || parsed.data.description !== undefined) { await prisma.group.update({ where: { id }, data: { ...(parsed.data.name !== undefined ? { name: parsed.data.name } : {}), ...(parsed.data.description !== undefined ? { description: parsed.data.description || null } : {}), }, }); } if (parsed.data.members !== undefined) { const members = parsed.data.members; const known = await prisma.user.findMany({ where: { id: { in: members.map((m) => m.userId) } }, select: { id: true }, }); if (known.length !== members.length) { throw new QrRequestError( "Mindestens einer der ausgewählten Benutzer existiert nicht mehr.", ); } // In einer Transaktion, damit die Gruppe nie ohne Mitglieder dasteht, // weil das Setzen der neuen Liste fehlgeschlagen ist. await prisma.$transaction([ prisma.membership.deleteMany({ where: { groupId: id, userId: { notIn: members.map((m) => m.userId) } }, }), ...members.map((member) => prisma.membership.upsert({ where: { userId_groupId: { userId: member.userId, groupId: id }, }, create: { userId: member.userId, groupId: id, role: member.role }, update: { role: member.role }, }), ), ]); } return Response.json({ ok: true }); } catch (error) { return errorResponse(error); } } export async function DELETE( _request: NextRequest, context: Context, ): Promise { try { requirePermission(await getActor(), "instance:manage"); const { id } = await context.params; if (id === DEFAULT_GROUP_ID) { throw new QrRequestError( "Die Standardgruppe lässt sich nicht löschen — ohne sie hätten Codes bei ausgeschalteter Anmeldung keine Heimat.", ); } const group = await prisma.group.findUnique({ where: { id }, include: { _count: { select: { codes: true } } }, }); if (!group) { throw new QrRequestError("Diese Gruppe existiert nicht (mehr).", 404); } if (group._count.codes > 0) { throw new QrRequestError( `Diese Gruppe enthält noch ${group._count.codes} ${group._count.codes === 1 ? "Code" : "Codes"}. Lösche oder verschiebe sie zuerst — sonst würden gedruckte Codes ins Leere führen.`, ); } await prisma.group.delete({ where: { id } }); return new Response(null, { status: 204 }); } catch (error) { return errorResponse(error); } }