UNPKG

aethercall

Version:

A scalable WebRTC video calling API built with Node.js and OpenVidu

491 lines (442 loc) 18.7 kB
/** * Connection Management Routes * HTTP endpoints for connection token generation and management */ const express = require('express'); const { body, param, validationResult } = require('express-validator'); const logger = require('../../../utils/logger'); module.exports = (dependencies) => { const router = express.Router(); const { openviduAPI, storage, tokenManager } = dependencies; /** * Validation middleware */ const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ error: 'Validation Error', details: errors.array(), requestId: req.context?.requestId }); } next(); }; /** * Authentication middleware */ const authenticateToken = async (req, res, next) => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Unauthorized', message: 'Missing or invalid authorization header', requestId: req.context?.requestId }); } const token = authHeader.substring(7); const tokenData = tokenManager.validateAPIToken(token); req.auth = tokenData; next(); } catch (error) { res.status(401).json({ error: 'Unauthorized', message: error.message, requestId: req.context?.requestId }); } }; /** * POST /api/connections * Create a connection token for a session */ router.post('/', authenticateToken, [ body('sessionId').isString().isLength({ min: 1 }), body('userId').isString().isLength({ min: 1 }), body('role').optional().isIn(['PUBLISHER', 'SUBSCRIBER', 'MODERATOR']), body('data').optional().isString(), body('record').optional().isBoolean(), body('metadata').optional().isObject(), body('kurentoOptions').optional().isObject() ], handleValidationErrors, async (req, res, next) => { try { const { sessionId, userId, role = 'PUBLISHER', data = '', record = true, metadata = {}, kurentoOptions = {} } = req.body; // Check if session exists const sessionData = await storage.getSession(sessionId); if (!sessionData) { return res.status(404).json({ error: 'Not Found', message: 'Session not found', requestId: req.context?.requestId }); } // Check if session is active if (sessionData.status !== 'active') { return res.status(400).json({ error: 'Bad Request', message: 'Session is not active', requestId: req.context?.requestId }); } // Create connection in OpenVidu const connectionOptions = { type: 'WEBRTC', role: role, data: data, record: record, kurentoOptions: kurentoOptions }; const openviduConnection = await openviduAPI.createConnection(sessionId, connectionOptions); // Generate session token for the client const sessionToken = tokenManager.generateSessionToken( sessionId, userId, role, metadata ); // Store connection data const connectionData = { connectionId: openviduConnection.id, sessionId: sessionId, userId: userId, role: role, data: data, record: record, metadata: metadata, status: 'pending', createdAt: new Date(), openviduData: openviduConnection }; // Update session with new connection if (!sessionData.connections) { sessionData.connections = []; } sessionData.connections.push(connectionData); await storage.updateSession(sessionId, { connections: sessionData.connections }); // Store user data if not exists const existingUser = await storage.getUser(userId); if (!existingUser) { await storage.storeUser(userId, { userId: userId, metadata: metadata, sessions: [sessionId] }); } logger.info(`Connection created for session ${sessionId}`, { connectionId: openviduConnection.id, userId: userId, role: role, clientId: req.auth.clientId, requestId: req.context?.requestId }); res.status(201).json({ success: true, data: { connectionId: openviduConnection.id, sessionId: sessionId, userId: userId, role: role, token: openviduConnection.token, sessionToken: sessionToken, metadata: metadata, createdAt: connectionData.createdAt }, requestId: req.context?.requestId }); } catch (error) { logger.error('Error creating connection:', error); next(error); } } ); /** * GET /api/connections/:connectionId * Get connection details */ router.get('/:connectionId', authenticateToken, [ param('connectionId').isString().isLength({ min: 1 }) ], handleValidationErrors, async (req, res, next) => { try { const connectionId = req.params.connectionId; // Find connection across all sessions const sessions = await storage.getActiveSessions(); let connectionData = null; let sessionId = null; for (const session of sessions) { if (session.connections) { const connection = session.connections.find( conn => conn.connectionId === connectionId ); if (connection) { connectionData = connection; sessionId = session.sessionId; break; } } } if (!connectionData) { return res.status(404).json({ error: 'Not Found', message: 'Connection not found', requestId: req.context?.requestId }); } res.json({ success: true, data: { connectionId: connectionData.connectionId, sessionId: sessionId, userId: connectionData.userId, role: connectionData.role, status: connectionData.status, metadata: connectionData.metadata, createdAt: connectionData.createdAt }, requestId: req.context?.requestId }); } catch (error) { logger.error('Error fetching connection:', error); next(error); } } ); /** * POST /api/connections/validate-token * Validate a session token */ router.post('/validate-token', [ body('token').isString().isLength({ min: 1 }) ], handleValidationErrors, async (req, res, next) => { try { const { token } = req.body; // Validate session token const tokenData = tokenManager.validateSessionToken(token); // Check if session still exists and is active const sessionData = await storage.getSession(tokenData.sessionId); if (!sessionData || sessionData.status !== 'active') { return res.status(400).json({ error: 'Bad Request', message: 'Session is not active or does not exist', requestId: req.context?.requestId }); } res.json({ success: true, data: { valid: true, sessionId: tokenData.sessionId, userId: tokenData.userId, role: tokenData.role, permissions: tokenData.permissions, metadata: tokenData.metadata }, requestId: req.context?.requestId }); } catch (error) { res.status(400).json({ success: false, data: { valid: false, error: error.message }, requestId: req.context?.requestId }); } } ); /** * POST /api/connections/join-room * Join a room with a room code */ router.post('/join-room', [ body('roomCode').isString().isLength({ min: 1 }), body('userId').isString().isLength({ min: 1 }), body('displayName').optional().isString(), body('metadata').optional().isObject() ], handleValidationErrors, async (req, res, next) => { try { const { roomCode, userId, displayName, metadata = {} } = req.body; // Find session by room code (look for both waiting and active sessions) const allSessions = await storage.getAllSessions(); const session = allSessions.find(s => s.metadata && s.metadata.roomCode === roomCode && (s.status === 'waiting' || s.status === 'active') ); if (!session) { return res.status(404).json({ error: 'Not Found', message: 'Room not found or inactive', requestId: req.context?.requestId }); } // If the session is waiting, activate it when first user joins if (session.status === 'waiting') { // Create the actual OpenVidu session const sessionOptions = { mediaMode: 'ROUTED', recordingMode: session.recordingMode || 'MANUAL', customSessionId: session.roomId, metadata: session.metadata }; try { const openviduSession = await openviduAPI.createSession(sessionOptions); // Update session status and add OpenVidu data session.status = 'active'; session.openviduSessionId = openviduSession.sessionId; session.openviduData = openviduSession; await storage.updateSession(session.roomId, session); logger.info(`Room ${roomCode} activated with OpenVidu session ${openviduSession.sessionId}`, { roomId: session.roomId, requestId: req.context?.requestId }); } catch (error) { logger.error('Failed to create OpenVidu session for room:', error); return res.status(500).json({ error: 'Internal Server Error', message: 'Failed to activate room', requestId: req.context?.requestId }); } } // Create connection for the user const connectionOptions = { type: 'WEBRTC', role: 'PUBLISHER', data: JSON.stringify({ displayName, ...metadata }), record: true }; const openviduConnection = await openviduAPI.createConnection( session.openviduSessionId || session.sessionId || session.roomId, connectionOptions ); // Generate session token const actualSessionId = session.openviduSessionId || session.sessionId || session.roomId; const sessionToken = tokenManager.generateSessionToken( actualSessionId, userId, 'PUBLISHER', { displayName, ...metadata } ); // Update session connections const connectionData = { connectionId: openviduConnection.id, sessionId: actualSessionId, userId: userId, role: 'PUBLISHER', data: JSON.stringify({ displayName, ...metadata }), record: true, metadata: { displayName, ...metadata }, status: 'pending', createdAt: new Date(), openviduData: openviduConnection }; if (!session.connections) { session.connections = []; } session.connections.push(connectionData); await storage.updateSession(session.roomId || session.sessionId, { connections: session.connections }); logger.info(`User ${userId} joined room ${roomCode}`, { sessionId: actualSessionId, roomId: session.roomId, connectionId: openviduConnection.id, requestId: req.context?.requestId }); res.json({ success: true, data: { sessionId: actualSessionId, connectionId: openviduConnection.id, token: openviduConnection.token, sessionToken: sessionToken, roomCode: roomCode, displayName: displayName, userId: userId }, requestId: req.context?.requestId }); } catch (error) { logger.error('Error joining room:', error); next(error); } } ); /** * DELETE /api/connections/:connectionId * Disconnect a connection */ router.delete('/:connectionId', authenticateToken, [ param('connectionId').isString().isLength({ min: 1 }) ], handleValidationErrors, async (req, res, next) => { try { const connectionId = req.params.connectionId; // Find and remove connection from all sessions const sessions = await storage.getActiveSessions(); let found = false; for (const session of sessions) { if (session.connections) { const initialLength = session.connections.length; session.connections = session.connections.filter( conn => conn.connectionId !== connectionId ); if (session.connections.length < initialLength) { await storage.updateSession(session.sessionId, { connections: session.connections }); found = true; break; } } } if (!found) { return res.status(404).json({ error: 'Not Found', message: 'Connection not found', requestId: req.context?.requestId }); } logger.info(`Connection ${connectionId} disconnected`, { clientId: req.auth.clientId, requestId: req.context?.requestId }); res.json({ success: true, message: 'Connection disconnected successfully', requestId: req.context?.requestId }); } catch (error) { logger.error('Error disconnecting connection:', error); next(error); } } ); return router; };