feat: add meeting creation and dashboard with join links

Adds the management/CRUD layer for meetings: a signed JWT join-link
token service (jose), instant/scheduled meeting creation endpoints,
and a protected /dashboard page listing the host's active, upcoming,
and past meetings with a "Nueva reunión" action area (instant button +
scheduling dialog). Also fixes a Base UI console warning (missing
nativeButton={false}) on Button instances rendered as Link.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
WilandClaude Sonnet 5 committed 2026-10-29 11:57:42 -05:00
1 parent 6facce3d22
commit e26cfe624d
14 files changed
+715 -1

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
import crypto from "node:crypto"
import { NextResponse } from "next/server"
import { auth } from "@/auth"
import { prisma } from "@/lib/prisma"
import { createJoinLinkToken } from "@/lib/join-link"
export async function POST() {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: "No autenticado" }, { status: 401 })
}
const hostUserId = Number(session.user.id)
const now = new Date()
// The creator is about to join immediately, so the meeting starts
// `active` right away rather than sitting in `scheduled` until someone
// joins.
const meeting = await prisma.meeting.create({
data: {
uuid: crypto.randomUUID(),
title: "Reunión instantánea",
type: "instant",
status: "active",
hostUserId,
startedAt: now,
},
})
const token = await createJoinLinkToken(meeting.uuid)
return NextResponse.json(
{
meeting: { id: meeting.id, uuid: meeting.uuid },
joinUrl: `/join/${meeting.uuid}?token=${token}`,
},
{ status: 201 }
)
}
+63
View File
@@ -0,0 +1,63 @@
import crypto from "node:crypto"
import { NextResponse } from "next/server"
import { auth } from "@/auth"
import { prisma } from "@/lib/prisma"
import { createJoinLinkToken } from "@/lib/join-link"
import { scheduledMeetingSchema } from "@/lib/validations/meeting"
export async function POST(request: Request) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: "No autenticado" }, { status: 401 })
}
let body: unknown
try {
body = await request.json()
} catch {
return NextResponse.json(
{ error: "Cuerpo de solicitud inválido" },
{ status: 400 }
)
}
const parsed = scheduledMeetingSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{
error: "Datos inválidos",
issues: parsed.error.flatten().fieldErrors,
},
{ status: 400 }
)
}
const hostUserId = Number(session.user.id)
const scheduledAt = new Date(parsed.data.scheduledAt)
const meeting = await prisma.meeting.create({
data: {
uuid: crypto.randomUUID(),
title: parsed.data.title,
type: "scheduled",
status: "scheduled",
hostUserId,
scheduledAt,
},
})
const token = await createJoinLinkToken(meeting.uuid, { scheduledAt })
return NextResponse.json(
{
meeting: { id: meeting.id, uuid: meeting.uuid },
joinUrl: `/join/${meeting.uuid}?token=${token}`,
},
{ status: 201 }
)
}