Beliebig viele Ziele je Konto, jedes mit eigenen Anlaessen. Jedes laesst sich einzeln testen und pausieren; Ergebnis und Zeitpunkt der letzten Zustellung stehen an der Karte. Der Weg laesst sich nach dem Anlegen nicht mehr aendern - ein Webhook-Ziel zu einer E-Mail-Adresse umzubiegen ergaebe keinen Sinn und muesste das Geheimnis mitwandern lassen. Ist kein SMTP hinterlegt, ist die Auswahl "E-Mail" gesperrt und erklaert, warum. targetSafeSelect liegt in lib/notify/types statt in der Route: Next laesst in route.ts nur bestimmte Exporte zu und bricht den Bau bei allem anderen ab. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { requireUserApi } from "@/lib/auth/guard";
|
|
import { encryptSecret, tokenHint } from "@/lib/crypto";
|
|
import { prisma } from "@/lib/db";
|
|
import { jsonError, readJsonBody } from "@/lib/security/request";
|
|
import { BlockedHostError, assertAllowedUrl } from "@/lib/security/ssrf";
|
|
import { targetSafeSelect } from "@/lib/notify/types";
|
|
import { formatZodError, notificationTargetUpdateSchema } from "@/lib/validation";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type Params = { params: Promise<{ id: string }> };
|
|
|
|
export async function PATCH(request: Request, { params }: Params) {
|
|
const auth = await requireUserApi();
|
|
if (!auth.ok) return auth.response;
|
|
|
|
const { id } = await params;
|
|
const existing = await prisma.notificationTarget.findFirst({
|
|
where: { id, userId: auth.user.id },
|
|
});
|
|
if (!existing) return jsonError("Ziel nicht gefunden", 404);
|
|
|
|
const body = await readJsonBody(request);
|
|
if (!body.ok) return body.response;
|
|
|
|
const parsed = notificationTargetUpdateSchema.safeParse(body.data);
|
|
if (!parsed.success) {
|
|
return NextResponse.json(
|
|
{ error: "Validierung fehlgeschlagen", fields: formatZodError(parsed.error) },
|
|
{ status: 422 },
|
|
);
|
|
}
|
|
|
|
const data = parsed.data;
|
|
const update: Record<string, unknown> = {};
|
|
|
|
if (data.name !== undefined) update.name = data.name;
|
|
if (data.events !== undefined) update.events = data.events;
|
|
if (data.isActive !== undefined) update.isActive = data.isActive;
|
|
|
|
if (data.target !== undefined) {
|
|
if (existing.channel !== "EMAIL") {
|
|
try {
|
|
await assertAllowedUrl(data.target);
|
|
} catch (error) {
|
|
if (error instanceof BlockedHostError) {
|
|
return NextResponse.json(
|
|
{ error: error.message, fields: { target: error.message } },
|
|
{ status: 422 },
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
update.target = data.target;
|
|
}
|
|
|
|
// Leeres Feld heisst "unveraendert", null heisst "entfernen".
|
|
if (data.secret !== undefined) {
|
|
if (data.secret) {
|
|
update.secretCipher = encryptSecret(data.secret);
|
|
update.secretHint = tokenHint(data.secret);
|
|
} else {
|
|
update.secretCipher = null;
|
|
update.secretHint = null;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const target = await prisma.notificationTarget.update({
|
|
where: { id },
|
|
data: update,
|
|
select: targetSafeSelect,
|
|
});
|
|
return NextResponse.json({ target });
|
|
} catch {
|
|
return jsonError("Name bereits vergeben.", 409);
|
|
}
|
|
}
|
|
|
|
export async function DELETE(_request: Request, { params }: Params) {
|
|
const auth = await requireUserApi();
|
|
if (!auth.ok) return auth.response;
|
|
|
|
const { id } = await params;
|
|
const result = await prisma.notificationTarget.deleteMany({
|
|
where: { id, userId: auth.user.id },
|
|
});
|
|
if (result.count === 0) return jsonError("Ziel nicht gefunden", 404);
|
|
|
|
return NextResponse.json({ ok: true });
|
|
}
|