fix: harden booking and meeting flows
This commit is contained in:
@@ -2,6 +2,8 @@ import { NextResponse } from "next/server";
|
||||
import { getPayload } from "payload";
|
||||
import config from "@payload-config";
|
||||
import { getAvailableSlots } from "@/lib/booking/slots";
|
||||
import { withBookingTransaction } from "@/lib/booking/transaction";
|
||||
import { checkRateLimit, getClientIp } from "@/lib/auth/rateLimit";
|
||||
import { berlinDayBounds } from "@/lib/calendar/timezone";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -17,13 +19,15 @@ export async function POST(request: Request) {
|
||||
let body: Body;
|
||||
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 offerSlug = typeof body.offerSlug === "string" ? body.offerSlug : "";
|
||||
const startISO = typeof body.start === "string" ? body.start : "";
|
||||
const appointmentType = body.appointmentType === "online" ? "online" : "onsite";
|
||||
const appointmentType = body.appointmentType;
|
||||
if (appointmentType !== "online" && appointmentType !== "onsite") return NextResponse.json({ error: "Ungültige Terminart." }, { status: 400 });
|
||||
const message = typeof body.message === "string" ? body.message.trim().slice(0, 2000) : "";
|
||||
|
||||
if (!offerSlug || !startISO) {
|
||||
@@ -40,6 +44,11 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "Bitte bestätige zuerst deine E-Mail-Adresse.", code: "EMAIL_NOT_VERIFIED" }, { status: 403 });
|
||||
}
|
||||
|
||||
if (!checkRateLimit(`booking:ip:${getClientIp(request)}`, { max: 30, windowMs: 3600000 }) ||
|
||||
!checkRateLimit(`booking:customer:${user.id}`, { max: 10, windowMs: 3600000 })) {
|
||||
return NextResponse.json({ error: "Zu viele Terminanfragen. Bitte versuche es später erneut." }, { status: 429 });
|
||||
}
|
||||
|
||||
const { docs } = await payload.find({
|
||||
collection: "offers",
|
||||
where: { slug: { equals: offerSlug } },
|
||||
@@ -61,32 +70,44 @@ export async function POST(request: Request) {
|
||||
// stale slot list) — the hard, race-safe check still runs again when
|
||||
// Anna confirms (see collections/BookingRequests.ts).
|
||||
const dayBounds = berlinDayBounds(start);
|
||||
const slotsByDay = await getAvailableSlots(payload, {
|
||||
durationMinutes: offer.durationMinutes,
|
||||
from: dayBounds.start,
|
||||
to: dayBounds.end,
|
||||
});
|
||||
const key = dayBounds.key;
|
||||
const isStillFree = (slotsByDay.get(key) ?? []).some((s) => s.start.getTime() === start.getTime());
|
||||
if (!isStillFree) {
|
||||
return NextResponse.json({ error: "Dieser Termin ist leider nicht mehr verfügbar." }, { status: 409 });
|
||||
}
|
||||
return await withBookingTransaction(payload, dayBounds.key, async (req) => {
|
||||
const { docs: existing } = await payload.find({ collection: "booking-requests", req, depth: 0, limit: 1,
|
||||
where: { and: [
|
||||
{ user: { equals: user.id } }, { offer: { equals: offer.id } },
|
||||
{ startTime: { equals: start.toISOString() } }, { appointmentType: { equals: appointmentType } },
|
||||
{ status: { in: ["pending", "confirmed"] } },
|
||||
] },
|
||||
});
|
||||
if (existing[0]) return NextResponse.json({ ok: true });
|
||||
const slotsByDay = await getAvailableSlots(payload, {
|
||||
durationMinutes: offer.durationMinutes!,
|
||||
req,
|
||||
from: dayBounds.start,
|
||||
to: dayBounds.end,
|
||||
});
|
||||
const key = dayBounds.key;
|
||||
const isStillFree = (slotsByDay.get(key) ?? []).some((s) => s.start.getTime() === start.getTime());
|
||||
if (!isStillFree) {
|
||||
return NextResponse.json({ error: "Dieser Termin ist leider nicht mehr verfügbar." }, { status: 409 });
|
||||
}
|
||||
|
||||
await payload.create({
|
||||
collection: "booking-requests",
|
||||
data: {
|
||||
user: user.id,
|
||||
offer: offer.id,
|
||||
date: start.toISOString(),
|
||||
startTime: start.toISOString(),
|
||||
endTime: end.toISOString(),
|
||||
appointmentType,
|
||||
status: "pending",
|
||||
userMessage: message || undefined,
|
||||
},
|
||||
});
|
||||
await payload.create({
|
||||
collection: "booking-requests",
|
||||
req,
|
||||
data: {
|
||||
user: user.id,
|
||||
offer: offer.id,
|
||||
date: start.toISOString(),
|
||||
startTime: start.toISOString(),
|
||||
endTime: end.toISOString(),
|
||||
appointmentType,
|
||||
status: "pending",
|
||||
userMessage: message || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
return NextResponse.json({ ok: true });
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("booking request failed", error);
|
||||
return NextResponse.json({ error: "Die Anfrage konnte nicht gesendet werden. Bitte versuche es später erneut." }, { status: 500 });
|
||||
|
||||
Reference in New Issue
Block a user