Elf Tests halten fest, was nicht durchkommen darf: script-Elemente, Ereignis-Attribute aus rohem HTML, javascript: als Link und als Bildquelle, data:text/html. Dazu die Aufloesung relativer Verweise und der Fall "Bild im Link", der kein a im a erzeugen darf. Gerendert wird serverseitig - fuer die Frage nach dem erzeugten HTML braucht es kein DOM. Damit .tsx ueberhaupt laeuft, muss Vitest das JSX selbst uebersetzen; die tsconfig setzt jsx auf "preserve", weil sonst Next zustaendig ist. Vite nimmt inzwischen oxc statt esbuild, die esbuild-Option wuerde stillschweigend ignoriert. Umsortieren im Brett und das Anlegen von Vorlagen hatten als einzige schreibende Endpunkte keine Ratenbegrenzung. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
import { NextResponse } from "next/server";
|
||
import { requireUserApi } from "@/lib/auth/guard";
|
||
import { prisma } from "@/lib/db";
|
||
import { LIMITS, rateLimit } from "@/lib/security/rateLimit";
|
||
import { clientIp, jsonError, readJsonBody, tooManyRequests } from "@/lib/security/request";
|
||
import { formatZodError, templateFromProjectSchema } from "@/lib/validation";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
/**
|
||
* Aus einem vorhandenen Projekt eine Vorlage machen.
|
||
*
|
||
* Das ist der Weg, auf dem Vorlagen ueblicherweise entstehen: man richtet ein
|
||
* Projekt so ein, wie es passt, und haelt das dann fest. Eine Vorlage von
|
||
* Grund auf zu tippen kann man auch – ueber POST /api/templates.
|
||
*
|
||
* Nicht mitgenommen wird alles Projektbezogene: Name, Slug, Fortschritt,
|
||
* Favorit und vor allem die Repositories. Eine Vorlage, die jedes neue
|
||
* Projekt an dasselbe Repository haengt, waere eine Falle.
|
||
*/
|
||
export async function POST(request: Request) {
|
||
const auth = await requireUserApi();
|
||
if (!auth.ok) return auth.response;
|
||
|
||
const limited = rateLimit(
|
||
`mutate:${clientIp(request)}`,
|
||
LIMITS.mutation.limit,
|
||
LIMITS.mutation.windowMs,
|
||
);
|
||
if (!limited.ok) return tooManyRequests(limited.retryAfter);
|
||
|
||
const body = await readJsonBody(request);
|
||
if (!body.ok) return body.response;
|
||
|
||
const parsed = templateFromProjectSchema.safeParse(body.data);
|
||
if (!parsed.success) {
|
||
return NextResponse.json(
|
||
{ error: "Validierung fehlgeschlagen", fields: formatZodError(parsed.error) },
|
||
{ status: 422 },
|
||
);
|
||
}
|
||
|
||
const { projectId, name, description, withNotes, withTasks } = parsed.data;
|
||
|
||
const project = await prisma.project.findFirst({
|
||
where: { id: projectId, ownerId: auth.user.id },
|
||
include: {
|
||
notes: { orderBy: [{ pinned: "desc" }, { createdAt: "asc" }] },
|
||
tasks: { orderBy: [{ status: "asc" }, { position: "asc" }] },
|
||
},
|
||
});
|
||
if (!project) return jsonError("Projekt nicht gefunden", 404);
|
||
|
||
const doppelt = await prisma.projectTemplate.findFirst({
|
||
where: { ownerId: auth.user.id, name },
|
||
select: { id: true },
|
||
});
|
||
if (doppelt) return jsonError(`Eine Vorlage „${name}“ gibt es bereits.`, 409);
|
||
|
||
const template = await prisma.projectTemplate.create({
|
||
data: {
|
||
ownerId: auth.user.id,
|
||
name,
|
||
description: description ?? null,
|
||
summary: project.summary,
|
||
projectDescription: project.description,
|
||
status: project.status,
|
||
priority: project.priority,
|
||
accent: project.accent,
|
||
tags: project.tags,
|
||
progressFromTasks: project.progressFromTasks,
|
||
notes: withNotes
|
||
? project.notes.map((note) => ({
|
||
title: note.title,
|
||
content: note.content,
|
||
pinned: note.pinned,
|
||
}))
|
||
: [],
|
||
// Erledigte Aufgaben kommen nicht mit: eine Vorlage beschreibt, was zu
|
||
// tun ist, nicht was einmal getan wurde.
|
||
tasks: withTasks
|
||
? project.tasks
|
||
.filter((task) => task.status !== "DONE")
|
||
.map((task) => ({
|
||
title: task.title,
|
||
description: task.description,
|
||
status: task.status,
|
||
labels: task.labels,
|
||
}))
|
||
: [],
|
||
},
|
||
});
|
||
|
||
return NextResponse.json({ template }, { status: 201 });
|
||
}
|