import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; import { prisma, getTenantPrisma } from '@/lib/prisma'; import { generateJitsiToken, generateJitsiMeetingUrl, JITSI_DOMAIN, } from '@/lib/jitsi'; const JoinRequestSchema = z.object({ userId: z.string().min(1, 'userId is required'), role: z.enum(['INSTRUCTOR', 'CLIENT', 'SUPER_ADMIN']).optional(), }); export async function POST( req: NextRequest, context: { params: Promise<{ id: string }> } ) { try { const { id: classSessionId } = await context.params; const body = await req.json(); const validated = JoinRequestSchema.parse(body); // 1. Fetch ClassSession with its parent Tenant and existing bookings const session = await prisma.classSession.findUnique({ where: { id: classSessionId }, include: { tenant: true, bookings: { include: { user: true, }, }, }, }); if (!session) { return NextResponse.json( { error: 'Class session not found' }, { status: 404 } ); } // 2. Fetch User and verify tenant membership const user = await prisma.user.findUnique({ where: { id: validated.userId }, }); if (!user) { return NextResponse.json( { error: 'User not found' }, { status: 404 } ); } if (user.tenantId !== session.tenantId) { return NextResponse.json( { error: 'Cross-tenant access forbidden. User does not belong to this tenant.' }, { status: 403 } ); } const effectiveRole = validated.role || user.role; const isInstructor = effectiveRole === 'INSTRUCTOR' || effectiveRole === 'SUPER_ADMIN'; // 3. For CLIENT, validate booking or enroll if space is available let booking = session.bookings.find((b) => b.userId === user.id); if (!isInstructor) { if (!booking) { // Check capacity if (session.bookings.length >= session.capacity) { return NextResponse.json( { error: 'Class is full. Maximum capacity reached.' }, { status: 409 } ); } // Auto-enroll client into booking const tenantPrisma = getTenantPrisma(session.tenantId); booking = await tenantPrisma.booking.create({ data: { tenantId: session.tenantId, userId: user.id, classSessionId: session.id, status: 'CONFIRMED', signedWaiverAt: new Date(), }, include: { user: true, }, }); } else if (booking.status === 'CANCELLED') { return NextResponse.json( { error: 'Booking for this class has been cancelled.' }, { status: 403 } ); } } // 4. Generate JWT room authentication token const token = generateJitsiToken({ roomName: session.jitsiRoomId, tenantSlug: session.tenant.slug, user: { id: user.id, name: user.name || (isInstructor ? 'Instructor' : 'Athlete'), email: user.email, role: effectiveRole, }, expiresInSeconds: 7200, // 2 hours }); // 5. Construct full meeting URL const meetingUrl = generateJitsiMeetingUrl(session.jitsiRoomId, token); return NextResponse.json({ success: true, roomName: session.jitsiRoomId, token, meetingUrl, domain: JITSI_DOMAIN, user: { id: user.id, name: user.name || 'Participant', email: user.email, role: effectiveRole, isHost: isInstructor, }, session: { id: session.id, title: session.title, description: session.description, startTime: session.startTime, endTime: session.endTime, capacity: session.capacity, currentParticipants: session.bookings.length, }, tenant: { id: session.tenant.id, name: session.tenant.name, slug: session.tenant.slug, }, }); } catch (error: any) { if (error instanceof z.ZodError) { return NextResponse.json( { error: 'Validation failed', details: error.issues }, { status: 400 } ); } console.error('Error joining classroom:', error); return NextResponse.json( { error: error.message || 'Internal server error' }, { status: 500 } ); } }