218 lines
5.2 KiB
TypeScript
218 lines
5.2 KiB
TypeScript
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>
|
|
)
|
|
}
|