diff --git a/app/(payload)/api/admin/meetings/test/route.ts b/app/(payload)/api/admin/meetings/test/route.ts new file mode 100644 index 0000000..f2b2b49 --- /dev/null +++ b/app/(payload)/api/admin/meetings/test/route.ts @@ -0,0 +1,33 @@ +import { randomUUID } from "node:crypto"; +import { NextResponse } from "next/server"; +import { getPayload } from "payload"; +import config from "@payload-config"; +import { createMeetingToken } from "@/lib/meeting/token"; + +export const dynamic = "force-dynamic"; + +/** Creates a short-lived, database-free test room for an authenticated admin. */ +export async function POST(request: Request) { + try { + const payload = await getPayload({ config }); + const { user } = await payload.auth({ headers: request.headers }); + if (!user || user.collection !== "users" || user.role !== "admin") { + return NextResponse.json({ error: "Nicht autorisiert." }, { status: 401 }); + } + + const eventSlug = `test-${randomUUID()}`; + const { token } = createMeetingToken({ + eventSlug, + name: typeof user.name === "string" && user.name.trim() ? user.name.trim() : "Host", + role: "host", + ttlMs: 60 * 60 * 1000, + }); + const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || new URL(request.url).origin; + const testUrl = `${baseUrl.replace(/\/$/, "")}/termine/${encodeURIComponent(eventSlug)}/call#token=${encodeURIComponent(token)}`; + + return NextResponse.json({ testUrl }); + } catch (error) { + console.error("admin test meeting start failed", error); + return NextResponse.json({ error: "Der Test-Call konnte nicht gestartet werden." }, { status: 500 }); + } +} diff --git a/app/(payload)/api/meetings/[slug]/connect/route.ts b/app/(payload)/api/meetings/[slug]/connect/route.ts index b994279..6ea4758 100644 --- a/app/(payload)/api/meetings/[slug]/connect/route.ts +++ b/app/(payload)/api/meetings/[slug]/connect/route.ts @@ -23,9 +23,12 @@ export async function POST(request: Request, { params }: Args) { } try { - const payload = await getPayload({ config }); - const { docs } = await payload.find({ collection: "events", where: { slug: { equals: slug } }, limit: 1 }); - if (!docs[0]?.isOnline) return NextResponse.json({ error: "Dieser Online-Termin wurde nicht gefunden." }, { status: 404 }); + const isAdminTestRoom = meetingToken.role === "host" && slug.startsWith("test-"); + if (!isAdminTestRoom) { + const payload = await getPayload({ config }); + const { docs } = await payload.find({ collection: "events", where: { slug: { equals: slug } }, limit: 1 }); + if (!docs[0]?.isOnline) return NextResponse.json({ error: "Dieser Online-Termin wurde nicht gefunden." }, { status: 404 }); + } const participantToken = await createLiveKitToken({ eventSlug: slug, diff --git a/components/admin/AdminTestCallButton.tsx b/components/admin/AdminTestCallButton.tsx new file mode 100644 index 0000000..6cb00ef --- /dev/null +++ b/components/admin/AdminTestCallButton.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useState } from "react"; + +export function AdminTestCallButton() { + const [loading, setLoading] = useState(false); + const [copied, setCopied] = useState(false); + const [url, setUrl] = useState(null); + const [error, setError] = useState(null); + + async function createTestCall() { + setLoading(true); + setCopied(false); + setError(null); + try { + const response = await fetch("/api/admin/meetings/test", { + method: "POST", + credentials: "include", + }); + const data = (await response.json().catch(() => ({}))) as { testUrl?: string; error?: string }; + if (!response.ok || !data.testUrl) { + setError(data.error || "Der Test-Call konnte nicht gestartet werden."); + return; + } + + setUrl(data.testUrl); + try { + await navigator.clipboard.writeText(data.testUrl); + setCopied(true); + } catch { + setError("Der Link wurde erzeugt. Bitte kopiere ihn aus dem Feld."); + } + } catch { + setError("Verbindung fehlgeschlagen."); + } finally { + setLoading(false); + } + } + + return ( +
+ + {url && ( + event.currentTarget.select()} + style={{ width: "100%", marginTop: 6, padding: "5px 7px", border: "1px solid #e2e2e2", borderRadius: 5, fontSize: 11 }} + /> + )} + {error &&

{error}

} +
+ ); +} diff --git a/components/admin/CalendarNavLink.tsx b/components/admin/CalendarNavLink.tsx index 9f2145b..cd38201 100644 --- a/components/admin/CalendarNavLink.tsx +++ b/components/admin/CalendarNavLink.tsx @@ -1,23 +1,27 @@ import Link from "next/link"; +import { AdminTestCallButton } from "./AdminTestCallButton"; export function CalendarNavLink() { return ( -
- - 📅 Kalender - -
+ <> +
+ + 📅 Kalender + +
+ + ); }