feat: add LiveKit join flow with waiting room and host admission
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e26cfe624d
commit
3c19455bdd
16 files changed
+1367
No files matched your search
@@ -0,0 +1,217 @@
|
||||
import { cookies } from "next/headers"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import { auth } from "@/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { verifyJoinLinkToken } from "@/lib/join-link"
|
||||
import { guestIdentityCookieName } from "@/lib/participant-identity"
|
||||
import { AppShell } from "@/components/shell/app-shell"
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { JoinFlow } from "@/components/join/join-flow"
|
||||
|
||||
// Reads the session/cookies and queries the DB on every request to
|
||||
// resolve "who is this visitor and what's their access state" — same
|
||||
// reasoning as the dashboard page (see its own comment) for opting out
|
||||
// of Cache Components' static-shell validation.
|
||||
export const instant = false
|
||||
|
||||
function JoinShell({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title?: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<AppShell className="flex min-h-[80vh] items-center justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
{title ? (
|
||||
<h1 className="mb-4 text-center text-lg font-semibold text-foreground">
|
||||
{title}
|
||||
</h1>
|
||||
) : null}
|
||||
{children}
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
||||
function JoinMessage({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
}) {
|
||||
return (
|
||||
<JoinShell>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</JoinShell>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function JoinPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ meetingUuid: string }>
|
||||
searchParams: Promise<{ token?: string }>
|
||||
}) {
|
||||
const { meetingUuid } = await params
|
||||
const { token } = await searchParams
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<JoinMessage
|
||||
title="Enlace inválido"
|
||||
description="Falta el token de invitación en el enlace. Pide al anfitrión que te comparta el enlace completo."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const verified = await verifyJoinLinkToken(token)
|
||||
if (!verified || verified.meetingUuid !== meetingUuid) {
|
||||
return (
|
||||
<JoinMessage
|
||||
title="Enlace inválido o expirado"
|
||||
description="Este enlace de invitación ya no es válido. Pide al anfitrión un nuevo enlace."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const meeting = await prisma.meeting.findUnique({
|
||||
where: { uuid: meetingUuid },
|
||||
})
|
||||
|
||||
if (!meeting) {
|
||||
return (
|
||||
<JoinMessage
|
||||
title="Reunión no encontrada"
|
||||
description="No encontramos esta reunión. Verifica el enlace."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (meeting.status === "ended") {
|
||||
return (
|
||||
<JoinMessage
|
||||
title="Esta reunión ha finalizado"
|
||||
description="El anfitrión ya terminó esta reunión. No es posible unirse."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const session = await auth()
|
||||
|
||||
if (session?.user?.id) {
|
||||
const userId = Number(session.user.id)
|
||||
const identity = `user-${userId}`
|
||||
|
||||
if (meeting.hostUserId === userId) {
|
||||
await prisma.meetingParticipant.upsert({
|
||||
where: {
|
||||
meetingId_identity: { meetingId: meeting.id, identity },
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
meetingId: meeting.id,
|
||||
userId,
|
||||
identity,
|
||||
role: "host",
|
||||
status: "admitted",
|
||||
admittedAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
redirect(`/room/${meetingUuid}`)
|
||||
}
|
||||
|
||||
const participant = await prisma.meetingParticipant.findUnique({
|
||||
where: {
|
||||
meetingId_identity: { meetingId: meeting.id, identity },
|
||||
},
|
||||
})
|
||||
|
||||
if (participant) {
|
||||
if (participant.status === "admitted") {
|
||||
redirect(`/room/${meetingUuid}`)
|
||||
}
|
||||
|
||||
if (participant.status === "denied") {
|
||||
return (
|
||||
<JoinShell>
|
||||
<JoinFlow meetingUuid={meetingUuid} token={token} mode="denied" />
|
||||
</JoinShell>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<JoinShell>
|
||||
<JoinFlow meetingUuid={meetingUuid} token={token} mode="waiting" />
|
||||
</JoinShell>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<JoinShell>
|
||||
<JoinFlow
|
||||
meetingUuid={meetingUuid}
|
||||
token={token}
|
||||
mode="confirm"
|
||||
displayName={session.user.displayName}
|
||||
/>
|
||||
</JoinShell>
|
||||
)
|
||||
}
|
||||
|
||||
// Anonymous visitor: recognize a returning guest via cookie before
|
||||
// falling back to the name-entry form.
|
||||
const cookieStore = await cookies()
|
||||
const guestIdentity = cookieStore.get(
|
||||
guestIdentityCookieName(meetingUuid)
|
||||
)?.value
|
||||
|
||||
if (guestIdentity) {
|
||||
const participant = await prisma.meetingParticipant.findUnique({
|
||||
where: {
|
||||
meetingId_identity: { meetingId: meeting.id, identity: guestIdentity },
|
||||
},
|
||||
})
|
||||
|
||||
if (participant) {
|
||||
if (participant.status === "admitted") {
|
||||
redirect(`/room/${meetingUuid}`)
|
||||
}
|
||||
|
||||
if (participant.status === "denied") {
|
||||
return (
|
||||
<JoinShell>
|
||||
<JoinFlow meetingUuid={meetingUuid} token={token} mode="denied" />
|
||||
</JoinShell>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<JoinShell>
|
||||
<JoinFlow meetingUuid={meetingUuid} token={token} mode="waiting" />
|
||||
</JoinShell>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<JoinShell>
|
||||
<JoinFlow meetingUuid={meetingUuid} token={token} mode="name-entry" />
|
||||
</JoinShell>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user