Integrate LiveKit SFU and harden booking workflow

This commit is contained in:
2026-09-16 19:38:28 +02:00
parent f1cd07b3ad
commit d0864e2911
55 changed files with 1648 additions and 1234 deletions
+7 -5
View File
@@ -12,6 +12,7 @@ import {
} from "@/lib/email/bookingTemplates";
import { sendEmail } from "@/lib/email/sendBookingEmails";
import type { Customer, Event, Offer } from "@/payload-types";
import { berlinDateKey, berlinDayBounds, formatBerlinDate, formatBerlinTime } from "@/lib/calendar/timezone";
export const APPOINTMENT_TYPES = [
{ label: "Vor Ort", value: "onsite" },
@@ -26,15 +27,15 @@ export const BOOKING_STATUSES = [
] as const;
function dateKey(iso: string): string {
return new Date(iso).toISOString().slice(0, 10);
return berlinDateKey(new Date(iso));
}
function fmtDate(iso: string): string {
return new Date(iso).toLocaleDateString("de-DE", { day: "2-digit", month: "long", year: "numeric" });
return formatBerlinDate(new Date(iso), { day: "2-digit", month: "long", year: "numeric" });
}
function fmtTime(iso: string): string {
return new Date(iso).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
return formatBerlinTime(new Date(iso), { hour: "2-digit", minute: "2-digit" });
}
const appointmentTypeLabel = (value: string) =>
@@ -70,8 +71,9 @@ const handleConfirmation: CollectionBeforeChangeHook = async ({ data, originalDo
await acquireBookingDayLock(req, dateKey(date));
const dayStart = new Date(new Date(date).setHours(0, 0, 0, 0)).toISOString();
const dayEnd = new Date(new Date(date).setHours(23, 59, 59, 999)).toISOString();
const dayBounds = berlinDayBounds(new Date(date));
const dayStart = dayBounds.start.toISOString();
const dayEnd = dayBounds.end.toISOString();
const conflictClauses: Where[] = [
{ status: { equals: "confirmed" } },
{ date: { greater_than_equal: dayStart } },
+5 -2
View File
@@ -14,8 +14,11 @@ export const EventRegistrations: CollectionConfig = {
description: "Anmeldungen zu Terminen — wird beim öffentlichen Anmeldeformular befüllt.",
},
access: {
// Visitors register themselves through the public event page.
create: () => true,
// Public registrations must go through /api/meetings/[slug]/register,
// which verifies publication/registrationRequired, normalizes the email,
// and enforces the capacity check. The Local API used by that route
// intentionally overrides this access rule after those checks.
create: () => false,
read: isAdmin,
update: isAdmin,
delete: isAdmin,