feat: add standalone admin video call test
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div style={{ padding: "0 8px 8px" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void createTestCall()}
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "8px 12px",
|
||||
borderRadius: 6,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "#fff",
|
||||
background: "#8b616d",
|
||||
border: "1px solid #8b616d",
|
||||
cursor: loading ? "wait" : "pointer",
|
||||
opacity: loading ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{loading ? "Test-Link wird erzeugt …" : copied ? "✓ Test-Link kopiert" : "▶ Test-Videoanruf"}
|
||||
</button>
|
||||
{url && (
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={url}
|
||||
aria-label="Test-Videoanruf-Link"
|
||||
onFocus={(event) => event.currentTarget.select()}
|
||||
style={{ width: "100%", marginTop: 6, padding: "5px 7px", border: "1px solid #e2e2e2", borderRadius: 5, fontSize: 11 }}
|
||||
/>
|
||||
)}
|
||||
{error && <p style={{ margin: "6px 0 0", color: "#a33", fontSize: 11 }}>{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,27 @@
|
||||
import Link from "next/link";
|
||||
import { AdminTestCallButton } from "./AdminTestCallButton";
|
||||
|
||||
export function CalendarNavLink() {
|
||||
return (
|
||||
<div style={{ padding: "0 8px 8px" }}>
|
||||
<Link
|
||||
href="/admin/kalender"
|
||||
style={{
|
||||
display: "block",
|
||||
padding: "8px 12px",
|
||||
borderRadius: 6,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "#8b616d",
|
||||
textDecoration: "none",
|
||||
border: "1px solid #e2e2e2",
|
||||
}}
|
||||
>
|
||||
📅 Kalender
|
||||
</Link>
|
||||
</div>
|
||||
<>
|
||||
<div style={{ padding: "0 8px 8px" }}>
|
||||
<Link
|
||||
href="/admin/kalender"
|
||||
style={{
|
||||
display: "block",
|
||||
padding: "8px 12px",
|
||||
borderRadius: 6,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "#8b616d",
|
||||
textDecoration: "none",
|
||||
border: "1px solid #e2e2e2",
|
||||
}}
|
||||
>
|
||||
📅 Kalender
|
||||
</Link>
|
||||
</div>
|
||||
<AdminTestCallButton />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user