106 lines
4.0 KiB
TypeScript
106 lines
4.0 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { getPayload } from "payload";
|
|
import config from "@payload-config";
|
|
import { checkRateLimit, getClientIp } from "@/lib/auth/rateLimit";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
// Registration capacity is checked before the insert. Serialize registrations
|
|
// for the same event so two simultaneous requests cannot both observe the
|
|
// same free seat in this single-process deployment.
|
|
const registrationLocks = new Map<string, Promise<void>>();
|
|
|
|
async function withEventRegistrationLock<T>(eventId: string, action: () => Promise<T>): Promise<T> {
|
|
const previous = registrationLocks.get(eventId) ?? Promise.resolve();
|
|
let release!: () => void;
|
|
const current = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
registrationLocks.set(eventId, current);
|
|
await previous;
|
|
try {
|
|
return await action();
|
|
} finally {
|
|
release();
|
|
if (registrationLocks.get(eventId) === current) registrationLocks.delete(eventId);
|
|
}
|
|
}
|
|
|
|
type Args = { params: Promise<{ slug: string }> };
|
|
|
|
export async function POST(request: Request, { params }: Args) {
|
|
const { slug } = await params;
|
|
|
|
if (!checkRateLimit(`event-registration:ip:${getClientIp(request)}`, { max: 20, windowMs: 60 * 60 * 1000 })) {
|
|
return NextResponse.json({ error: "Zu viele Anmeldungen. Bitte versuche es später erneut." }, { status: 429 });
|
|
}
|
|
|
|
let body: { name?: unknown; email?: unknown };
|
|
try {
|
|
body = await request.json();
|
|
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("Invalid body");
|
|
} catch {
|
|
return NextResponse.json({ error: "Ungültige Anfrage." }, { status: 400 });
|
|
}
|
|
|
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
if (!name || name.length > 120 || !email || email.length > 254 || !email.includes("@")) {
|
|
return NextResponse.json({ error: "Bitte gib deinen Namen und eine gültige E-Mail-Adresse an." }, { status: 400 });
|
|
}
|
|
|
|
try {
|
|
const payload = await getPayload({ config });
|
|
|
|
const { docs } = await payload.find({
|
|
collection: "events",
|
|
where: { slug: { equals: slug } },
|
|
limit: 1,
|
|
});
|
|
const event = docs[0];
|
|
if (!event || event._status !== "published" || !event.registrationRequired) {
|
|
return NextResponse.json({ error: "Dieser Termin wurde nicht gefunden." }, { status: 404 });
|
|
}
|
|
if (!checkRateLimit(`event-registration:email:${event.id}:${email}`, { max: 3, windowMs: 60 * 60 * 1000 })) {
|
|
return NextResponse.json({ error: "Zu viele Anmeldeversuche für diese Adresse. Bitte versuche es später erneut." }, { status: 429 });
|
|
}
|
|
|
|
return await withEventRegistrationLock(String(event.id), async () => {
|
|
const { docs: existingForEmail } = await payload.find({
|
|
collection: "event-registrations",
|
|
where: {
|
|
and: [
|
|
{ event: { equals: event.id } },
|
|
{ email: { equals: email } },
|
|
{ status: { equals: "registered" } },
|
|
],
|
|
},
|
|
limit: 1,
|
|
});
|
|
if (existingForEmail[0]) {
|
|
return NextResponse.json({ ok: true, alreadyRegistered: true });
|
|
}
|
|
|
|
if (event.maxParticipants) {
|
|
const { totalDocs } = await payload.count({
|
|
collection: "event-registrations",
|
|
where: { and: [{ event: { equals: event.id } }, { status: { equals: "registered" } }] },
|
|
});
|
|
if (totalDocs >= event.maxParticipants) {
|
|
return NextResponse.json({ error: "Dieser Termin ist bereits ausgebucht." }, { status: 409 });
|
|
}
|
|
}
|
|
|
|
await payload.create({
|
|
collection: "event-registrations",
|
|
data: { event: event.id, name, email, status: "registered" },
|
|
});
|
|
|
|
return NextResponse.json({ ok: true });
|
|
});
|
|
} catch (error) {
|
|
console.error("event registration failed", error);
|
|
return NextResponse.json({ error: "Die Anmeldung ist gerade nicht möglich. Bitte versuche es später erneut." }, { status: 500 });
|
|
}
|
|
}
|