sar / src /app /api /session /route.ts
Mafia2008's picture
fix: make handleLookup async to fix bun next build error
bf40519
Raw
History Blame Contribute Delete
14.8 kB
// ═══════════════════════════════════════════════════════════════════════════
// 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<string | null> {
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())
});
}