159 lines
4.3 KiB
TypeScript
159 lines
4.3 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
} |