import type { NextRequest } from "next/server"; import { z } from "zod"; import { getActor } from "@/lib/auth/actor"; import { requirePermission } from "@/lib/auth/policy"; import { prisma } from "@/lib/db"; import { DEFAULT_DOMAIN_ID } from "@/lib/domains"; 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({ label: z.string().trim().max(80).nullable().optional(), isActive: z.boolean().optional(), /** * Vollständige Liste der Gruppen, die diese Domain nutzen dürfen. Wer * fehlt, verliert die Zuteilung. */ grants: z .array( z.object({ groupId: z.string().trim().min(1), isDefault: z.boolean().default(false), }), ) .optional(), }); export async function PATCH( request: NextRequest, context: Context, ): Promise { try { requirePermission(await getActor(), "instance:manage"); const { id } = await context.params; const domain = await prisma.domain.findUnique({ where: { id } }); if (!domain) { throw new QrRequestError("Diese Domain existiert nicht (mehr).", 404); } const parsed = updateSchema.safeParse(await request.json()); if (!parsed.success) { return jsonError(firstZodMessage(parsed.error)); } if (parsed.data.label !== undefined || parsed.data.isActive !== undefined) { await prisma.domain.update({ where: { id }, data: { ...(parsed.data.label !== undefined ? { label: parsed.data.label || null } : {}), ...(parsed.data.isActive !== undefined ? { isActive: parsed.data.isActive } : {}), }, }); } if (parsed.data.grants !== undefined) { const grants = parsed.data.grants; const known = await prisma.group.findMany({ where: { id: { in: grants.map((g) => g.groupId) } }, select: { id: true }, }); if (known.length !== grants.length) { throw new QrRequestError( "Mindestens eine der ausgewählten Gruppen existiert nicht mehr.", ); } // Eine Gruppe, die diese Domain noch für Codes verwendet, darf die // Zuteilung nicht verlieren — sonst zeigten deren Kurz-URLs auf eine // Domain, die der Gruppe gar nicht mehr gehört. const stillInUse = await prisma.qrCode.findMany({ where: { domainId: id, groupId: { notIn: grants.map((g) => g.groupId) } }, select: { group: { select: { name: true } } }, distinct: ["groupId"], }); if (stillInUse.length > 0) { const names = stillInUse.map((entry) => entry.group.name).join(", "); throw new QrRequestError( `Diese Gruppen haben noch Codes unter dieser Domain: ${names}. Solange die Codes bestehen, muss die Zuteilung bleiben.`, ); } await prisma.$transaction([ prisma.domainGrant.deleteMany({ where: { domainId: id, groupId: { notIn: grants.map((g) => g.groupId) } }, }), ...grants.map((grant) => prisma.domainGrant.upsert({ where: { domainId_groupId: { domainId: id, groupId: grant.groupId }, }, create: { domainId: id, groupId: grant.groupId, isDefault: grant.isDefault, }, update: { isDefault: grant.isDefault }, }), ), // Je Gruppe darf nur eine Domain die Voreinstellung sein. ...grants .filter((grant) => grant.isDefault) .map((grant) => prisma.domainGrant.updateMany({ where: { groupId: grant.groupId, domainId: { not: id } }, data: { isDefault: false }, }), ), ]); } 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_DOMAIN_ID) { throw new QrRequestError( "Die Standard-Domain lässt sich nicht löschen — sie fängt Aufrufe über nicht registrierte Adressen ab.", ); } const domain = await prisma.domain.findUnique({ where: { id }, include: { _count: { select: { codes: true } } }, }); if (!domain) { throw new QrRequestError("Diese Domain existiert nicht (mehr).", 404); } if (domain._count.codes > 0) { throw new QrRequestError( `Unter dieser Domain liegen noch ${domain._count.codes} ${domain._count.codes === 1 ? "Code" : "Codes"}. Sie zu löschen würde gedruckte Codes ins Leere laufen lassen — schalte sie stattdessen ab, wenn sie nicht mehr genutzt werden soll.`, ); } await prisma.domain.delete({ where: { id } }); return new Response(null, { status: 204 }); } catch (error) { return errorResponse(error); } }