aethercall
Version:
A scalable WebRTC video calling API built with Node.js and OpenVidu
491 lines (442 loc) • 18.7 kB
JavaScript
/**
* 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;
};