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 { ensureDefaultDomain, normalizeHostname } from "@/lib/domains"; import { errorResponse, firstZodMessage, jsonError } from "@/lib/http"; import { QrRequestError } from "@/lib/qr/type-module"; export const dynamic = "force-dynamic"; /** * Ein Hostname, optional mit Port. Bewusst ohne Schema und ohne Pfad — das * ist der Wert, den ein Browser im `Host`-Header sendet. */ const hostnameSchema = z .string() .trim() .min(1, { error: "Bitte gib einen Hostnamen ein." }) .max(253, { error: "Der Hostname ist zu lang." }) .transform(normalizeHostname) .refine((value) => /^[a-z0-9.-]+(:\d{1,5})?$/.test(value), { error: "Das ist kein gültiger Hostname. Erwartet wird z. B. qr.example.com — ohne https:// und ohne Pfad.", }) .refine((value) => value.includes(".") || value.startsWith("localhost"), { error: "Bitte gib den vollständigen Hostnamen an, z. B. qr.example.com", }); const createSchema = z.object({ hostname: hostnameSchema, label: z.string().trim().max(80).optional(), }); export async function GET(): Promise { try { requirePermission(await getActor(), "instance:manage"); // Sorgt dafür, dass die Standard-Domain den Hostnamen aus BASE_URL trägt. await ensureDefaultDomain(); const domains = await prisma.domain.findMany({ orderBy: { hostname: "asc" }, include: { grants: { include: { group: { select: { id: true, name: true } } } }, _count: { select: { codes: true } }, }, }); const groups = await prisma.group.findMany({ orderBy: { name: "asc" }, select: { id: true, name: true }, }); return Response.json({ domains: domains.map((domain) => ({ id: domain.id, hostname: domain.hostname, label: domain.label, isActive: domain.isActive, codeCount: domain._count.codes, grants: domain.grants.map((grant) => ({ groupId: grant.group.id, groupName: grant.group.name, isDefault: grant.isDefault, })), })), groups, }); } catch (error) { return errorResponse(error); } } export async function POST(request: NextRequest): Promise { try { requirePermission(await getActor(), "instance:manage"); const parsed = createSchema.safeParse(await request.json()); if (!parsed.success) { return jsonError(firstZodMessage(parsed.error)); } const existing = await prisma.domain.findUnique({ where: { hostname: parsed.data.hostname }, select: { id: true }, }); if (existing) { throw new QrRequestError("Diese Domain ist bereits registriert.", 409); } const domain = await prisma.domain.create({ data: { hostname: parsed.data.hostname, label: parsed.data.label || null, }, }); return Response.json( { domain: { id: domain.id, hostname: domain.hostname } }, { status: 201 }, ); } catch (error) { return errorResponse(error); } }