import { timingSafeEqual } from "node:crypto"; import { NextResponse } from "next/server"; import { getPayload } from "payload"; import config from "@payload-config"; import { createMeetingToken } from "@/lib/meeting/token"; import { authorizeMeeting } from "@/lib/meeting/access"; import { checkRateLimit, getClientIp } from "@/lib/auth/rateLimit"; import { createLiveKitToken, getLiveKitClientUrl } from "@/lib/meeting/livekit"; import { isMeetingParticipantKicked } from "@/lib/meeting/kicks"; import { verifyTurnstileToken } from "@/lib/turnstile/server"; export const dynamic = "force-dynamic"; function safeEqual(a: string, b: string): boolean { const bufA = Buffer.from(a); const bufB = Buffer.from(b); if (bufA.length !== bufB.length) return false; return timingSafeEqual(bufA, bufB); } type Args = { params: Promise<{ slug: string }> }; export async function POST(request: Request, { params }: Args) { const { slug } = await params; if (!checkRateLimit(`meeting-join:${getClientIp(request)}`, { max: 30, windowMs: 60000 })) { return NextResponse.json({ error: "Zu viele Beitrittsversuche." }, { status: 429 }); } let body: { name?: unknown; password?: unknown; clientId?: unknown; token?: 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 password = typeof body.password === "string" ? body.password.trim() : ""; const clientId = typeof body.clientId === "string" ? body.clientId.trim() : undefined; if (!name || name.length > 120) { return NextResponse.json({ error: "Bitte gib deinen Namen ein." }, { status: 400 }); } if (password.length > 200) { return NextResponse.json({ error: "Das Meeting-Passwort ist ungültig." }, { status: 400 }); } if (!(await verifyTurnstileToken(body.token, "meeting-join", request))) { return NextResponse.json({ error: "Sicherheitsprüfung fehlgeschlagen. Bitte versuche es erneut." }, { status: 403 }); } 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.isOnline) { return NextResponse.json({ error: "Dieser Online-Termin wurde nicht gefunden." }, { status: 404 }); } const { user } = await payload.auth({ headers: request.headers }); // Must check the collection, not just presence - a logged-in customer // (collection "customers") must never be treated as host. const isHost = user?.collection === "users" && user.role === "admin"; if (!isHost) { if (!password) { return NextResponse.json({ error: "Bitte gib das Meeting-Passwort ein." }, { status: 400 }); } if (!event.meetingPassword || !safeEqual(password, event.meetingPassword)) { return NextResponse.json({ error: "Das Meeting-Passwort ist nicht korrekt." }, { status: 401 }); } } const access = await authorizeMeeting(payload, event, isHost ? "host" : "participant"); if (!access) { return NextResponse.json({ error: "Dieser Termin ist nicht offen oder wurde storniert." }, { status: 403 }); } const { token, participantId } = createMeetingToken({ eventSlug: slug, name, role: isHost ? "host" : "participant", accessVersion: access.accessVersion, ttlMs: Math.min(6 * 3600000, access.expiresAt - Date.now()), // Only meaningful for participants - a host authenticates via a real // session already, so there's no need to anchor a kick to it (and // hosts can't be kicked in the first place. clientId: isHost ? undefined : clientId, }); if (!isHost && isMeetingParticipantKicked(slug, participantId, clientId)) { return NextResponse.json({ error: "Du wurdest aus diesem Meeting entfernt." }, { status: 403 }); } const participantToken = await createLiveKitToken({ eventSlug: slug, accessVersion: access.accessVersion, expiresAt: access.expiresAt, participantId, name, role: isHost ? "host" : "participant", clientId: isHost ? undefined : clientId, }); return NextResponse.json({ token, participantId, role: isHost ? "host" : "participant", eventTitle: event.title, participantToken, serverUrl: getLiveKitClientUrl(), }); } catch (error) { console.error("meeting join failed", error); return NextResponse.json({ error: "Der Beitritt ist gerade nicht möglich. Bitte versuche es später erneut." }, { status: 500 }); } }