// ═══════════════════════════════════════════════════════════════════════════ // SESSION API - Create, Join, End, Get sessions // ═══════════════════════════════════════════════════════════════════════════ // POST /api/session // Body: { action: 'create' | 'join' | 'end' | 'get', ... } // ═══════════════════════════════════════════════════════════════════════════ import { NextRequest, NextResponse } from 'next/server'; import { sessionManager } from '@/lib/session-manager'; export { OPTIONS } from '@/lib/cors'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; export async function POST(request: NextRequest) { try { const body = await request.json(); const { action } = body; switch (action) { case 'create': return handleCreate(body); case 'join': return handleJoin(body); case 'end': return handleEnd(body); case 'get': return handleGet(body); case 'list': return handleList(); case 'lookup': return handleLookup(body); default: return NextResponse.json({ success: false, error: `Invalid action: ${action}. Valid actions: create, join, end, get, list, lookup` }, { status: 400 }); } } catch (error) { return NextResponse.json({ success: false, error: 'Invalid JSON body' }, { status: 400 }); } } // ═══════════════════════════════════════════════════════════════════════════ // CREATE SESSION // ═══════════════════════════════════════════════════════════════════════════ // POST /api/session // Body: { // action: 'create', // teacherId: string, // Required: Unique teacher ID // teacherName: string, // Required: Teacher's display name // sessionName: string, // Required: Name of the session // description?: string, // Optional: Session description // slideUrls?: string[] // Optional: Array of slide image URLs // } // ═══════════════════════════════════════════════════════════════════════════ function handleCreate(body: any) { const { teacherId, teacherName, sessionName, description, slideUrls } = body; // Validate required fields if (!teacherId || !teacherName || !sessionName) { return NextResponse.json({ success: false, error: 'Missing required fields: teacherId, teacherName, sessionName' }, { status: 400 }); } // Create session const session = sessionManager.createSession( teacherId, teacherName, sessionName, description, slideUrls || [] ); return NextResponse.json({ success: true, data: session, message: 'Session created successfully' }); } // ═══════════════════════════════════════════════════════════════════════════ // LOOKUP SESSION BY ROOM CODE // ═══════════════════════════════════════════════════════════════════════════ // POST /api/session // Body: { action: 'lookup', roomCode: string } // ═══════════════════════════════════════════════════════════════════════════ async function handleLookup(body: any) { const { roomCode } = body; if (!roomCode) { return NextResponse.json({ success: false, error: 'Missing roomCode' }, { status: 400 }); } // Try exact room code first const byCode = sessionManager.getSessionByRoomCode(String(roomCode)); if (byCode) { return NextResponse.json({ success: true, data: byCode }); } // Fallback: treat as full session ID const byId = sessionManager.getSession(String(roomCode)); if (byId) { return NextResponse.json({ success: true, data: byId }); } // Try to find on disk if server restarted try { const diskId = await findSessionIdByRoomCode(String(roomCode)); if (diskId) { // Just return the lookup info, not the full session tree, or fetch it? // handleLookup usually returns the session object. const fs = await import('fs/promises'); const path = await import('path'); const storageRoot = process.env.SPACE_ID ? '/data' : path.join(process.cwd(), 'data'); const filePath = path.join(storageRoot, 'sessions', diskId, 'session_url.json'); const dataStr = await fs.readFile(filePath, 'utf-8'); const oldState = JSON.parse(dataStr); return NextResponse.json({ success: true, data: oldState.session }); } } catch (err) {} return NextResponse.json({ success: false, error: 'Session not found' }, { status: 404 }); } // Helper to scan disk for room codes after restart async function findSessionIdByRoomCode(roomCode: string): Promise { try { const fs = await import('fs/promises'); const path = await import('path'); const storageRoot = process.env.SPACE_ID ? '/data' : path.join(process.cwd(), 'data'); const sessionsDir = path.join(storageRoot, 'sessions'); const folders = await fs.readdir(sessionsDir); for (const folder of folders) { try { const filePath = path.join(sessionsDir, folder, 'session_url.json'); const dataStr = await fs.readFile(filePath, 'utf-8'); const state = JSON.parse(dataStr); if (state?.session?.roomCode === roomCode) { return folder; } } catch (err) { } } } catch (err) {} return null; } // ═══════════════════════════════════════════════════════════════════════════ // JOIN SESSION // ═══════════════════════════════════════════════════════════════════════════ // POST /api/session // Body: { // action: 'join', // sessionId: string, // Required: Session ID to join // userId: string, // Required: Unique user ID // userName: string // Required: User's display name // } // ═══════════════════════════════════════════════════════════════════════════ async function handleJoin(body: any) { let { sessionId, userId, userName } = body; // Support joining by 4-digit room code if (sessionId && sessionId.length <= 6) { const byCode = sessionManager.getSessionByRoomCode(sessionId); if (byCode) { sessionId = byCode.id; } else { const diskId = await findSessionIdByRoomCode(sessionId); if (diskId) sessionId = diskId; } } // Validate required fields if (!sessionId || !userId || !userName) { return NextResponse.json({ success: false, error: 'Missing required fields: sessionId, userId, userName' }, { status: 400 }); } // Check if session exists in memory const session = sessionManager.getSession(sessionId); if (!session || !session.isActive) { // Try to load from disk try { const fs = await import('fs/promises'); const path = await import('path'); const storageRoot = process.env.SPACE_ID ? '/data' : path.join(process.cwd(), 'data'); const filePath = path.join(storageRoot, 'sessions', sessionId, 'session_url.json'); const dataStr = await fs.readFile(filePath, 'utf-8'); const oldState = JSON.parse(dataStr); const user = { id: userId, name: userName, role: 'student', sessionId, joinedAt: new Date().toISOString() }; return NextResponse.json({ success: true, data: { user, ...oldState }, message: 'Joined past session' }); } catch (err) { return NextResponse.json({ success: false, error: 'Session not found or has ended' }, { status: 404 }); } } // Join session const user = sessionManager.joinSession(sessionId, userId, userName, 'student'); // Get current session state const state = sessionManager.getSessionState(sessionId); return NextResponse.json({ success: true, data: { user, session: state?.session, currentSlide: state?.currentSlide, whiteboardStrokes: state?.whiteboardStrokes, reactions: state?.reactions, activePoll: state?.activePoll }, message: 'Joined session successfully' }); } // ═══════════════════════════════════════════════════════════════════════════ // END SESSION // ═══════════════════════════════════════════════════════════════════════════ // POST /api/session // Body: { // action: 'end', // sessionId: string // Required: Session ID to end // } // ═══════════════════════════════════════════════════════════════════════════ async function handleEnd(body: any) { const { sessionId } = body; if (!sessionId) { return NextResponse.json({ success: false, error: 'Missing required field: sessionId' }, { status: 400 }); } const session = sessionManager.getSession(sessionId); if (!session) { return NextResponse.json({ success: false, error: 'Session not found' }, { status: 404 }); } await sessionManager.endSession(sessionId); return NextResponse.json({ success: true, message: 'Session ended successfully' }); } // ═══════════════════════════════════════════════════════════════════════════ // GET SESSION // ═══════════════════════════════════════════════════════════════════════════ // POST /api/session // Body: { // action: 'get', // sessionId: string // Required: Session ID to get // } // ═══════════════════════════════════════════════════════════════════════════ async function handleGet(body: any) { const { sessionId } = body; if (!sessionId) { return NextResponse.json({ success: false, error: 'Missing required field: sessionId' }, { status: 400 }); } const state = sessionManager.getSessionState(sessionId); if (!state) { try { const fs = await import('fs/promises'); const path = await import('path'); const storageRoot = process.env.SPACE_ID ? '/data' : path.join(process.cwd(), 'data'); const filePath = path.join(storageRoot, 'sessions', sessionId, 'session_url.json'); const dataStr = await fs.readFile(filePath, 'utf-8'); const oldState = JSON.parse(dataStr); return NextResponse.json({ success: true, data: oldState }); } catch (err) { return NextResponse.json({ success: false, error: 'Session not found' }, { status: 404 }); } } return NextResponse.json({ success: true, data: state }); } // ═══════════════════════════════════════════════════════════════════════════ // LIST ALL SESSIONS // ═══════════════════════════════════════════════════════════════════════════ // POST /api/session // Body: { action: 'list' } // ═══════════════════════════════════════════════════════════════════════════ async function handleList() { const activeSessions = sessionManager.getAllSessions(); const allSessions = [...activeSessions]; // Also read older sessions from disk try { const fs = await import('fs/promises'); const path = await import('path'); const storageRoot = process.env.SPACE_ID ? '/data' : path.join(process.cwd(), 'data'); const sessionsDir = path.join(storageRoot, 'sessions'); const folders = await fs.readdir(sessionsDir).catch(() => []); for (const folder of folders) { // Skip if already active in memory if (activeSessions.some(s => s.id === folder)) continue; try { const filePath = path.join(sessionsDir, folder, 'session_url.json'); const dataStr = await fs.readFile(filePath, 'utf-8'); const state = JSON.parse(dataStr); if (state?.session) { allSessions.push(state.session); } } catch (err) {} } } catch (err) {} return NextResponse.json({ success: true, data: allSessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) }); }