aethercall
Version:
A scalable WebRTC video calling API built with Node.js and OpenVidu
347 lines (313 loc) • 12.6 kB
JavaScript
/**
* Session Management Routes
* HTTP endpoints for session operations
*/
const express = require('express');
const { body, param, query, 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
});
}
};
/**
* GET /api/sessions
* Get all active sessions
*/
router.get('/',
authenticateToken,
async (req, res, next) => {
try {
const sessions = await storage.getActiveSessions();
res.json({
success: true,
data: sessions,
count: sessions.length,
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error fetching sessions:', error);
next(error);
}
}
);
/**
* POST /api/sessions
* Create a new session
*/
router.post('/',
authenticateToken,
[
body('mediaMode').optional().isIn(['ROUTED', 'RELAYED']),
body('recordingMode').optional().isIn(['ALWAYS', 'MANUAL', 'NEVER']),
body('customSessionId').optional().isString().isLength({ min: 1, max: 50 }),
body('defaultRecordingProperties').optional().isObject(),
body('metadata').optional().isObject()
],
handleValidationErrors,
async (req, res, next) => {
try {
const sessionOptions = {
mediaMode: req.body.mediaMode || 'ROUTED',
recordingMode: req.body.recordingMode || 'MANUAL',
customSessionId: req.body.customSessionId,
defaultRecordingProperties: req.body.defaultRecordingProperties || {},
metadata: req.body.metadata || {}
};
// Create session in OpenVidu
const openviduSession = await openviduAPI.createSession(sessionOptions);
// Store session data locally
const sessionData = {
sessionId: openviduSession.sessionId,
status: 'active',
mediaMode: sessionOptions.mediaMode,
recordingMode: sessionOptions.recordingMode,
metadata: sessionOptions.metadata,
createdBy: req.auth.clientId,
connections: [],
recordings: [],
openviduData: openviduSession
};
await storage.storeSession(openviduSession.sessionId, sessionData);
logger.info(`Session created: ${openviduSession.sessionId}`, {
clientId: req.auth.clientId,
requestId: req.context?.requestId
});
res.status(201).json({
success: true,
data: {
sessionId: openviduSession.sessionId,
status: 'active',
mediaMode: sessionOptions.mediaMode,
recordingMode: sessionOptions.recordingMode,
metadata: sessionOptions.metadata,
createdAt: sessionData.createdAt
},
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error creating session:', error);
next(error);
}
}
);
/**
* GET /api/sessions/:sessionId
* Get session details
*/
router.get('/:sessionId',
authenticateToken,
[
param('sessionId').isString().isLength({ min: 1 })
],
handleValidationErrors,
async (req, res, next) => {
try {
const sessionId = req.params.sessionId;
// Get session from local storage
const sessionData = await storage.getSession(sessionId);
if (!sessionData) {
return res.status(404).json({
error: 'Not Found',
message: 'Session not found',
requestId: req.context?.requestId
});
}
// Get fresh data from OpenVidu
try {
const openviduSession = await openviduAPI.getSession(sessionId);
sessionData.openviduData = openviduSession;
// Update local storage
await storage.updateSession(sessionId, {
openviduData: openviduSession,
lastSyncAt: new Date()
});
} catch (openviduError) {
// If OpenVidu session doesn't exist, mark as inactive
if (openviduError.message.includes('404')) {
await storage.updateSession(sessionId, { status: 'ended' });
sessionData.status = 'ended';
}
}
res.json({
success: true,
data: {
sessionId: sessionData.sessionId,
status: sessionData.status,
mediaMode: sessionData.mediaMode,
recordingMode: sessionData.recordingMode,
metadata: sessionData.metadata,
connections: sessionData.connections || [],
recordings: sessionData.recordings || [],
createdAt: sessionData.createdAt,
updatedAt: sessionData.updatedAt
},
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error fetching session:', error);
next(error);
}
}
);
/**
* PATCH /api/sessions/:sessionId
* Update session metadata
*/
router.patch('/:sessionId',
authenticateToken,
[
param('sessionId').isString().isLength({ min: 1 }),
body('metadata').optional().isObject()
],
handleValidationErrors,
async (req, res, next) => {
try {
const sessionId = req.params.sessionId;
const updates = {};
if (req.body.metadata) {
updates.metadata = req.body.metadata;
}
// Update local storage
await storage.updateSession(sessionId, updates);
logger.info(`Session updated: ${sessionId}`, {
updates,
clientId: req.auth.clientId,
requestId: req.context?.requestId
});
res.json({
success: true,
message: 'Session updated successfully',
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error updating session:', error);
next(error);
}
}
);
/**
* DELETE /api/sessions/:sessionId
* Close a session
*/
router.delete('/:sessionId',
authenticateToken,
[
param('sessionId').isString().isLength({ min: 1 })
],
handleValidationErrors,
async (req, res, next) => {
try {
const sessionId = req.params.sessionId;
// Close session in OpenVidu
try {
await openviduAPI.closeSession(sessionId);
} catch (openviduError) {
// Session might already be closed in OpenVidu
logger.warn(`OpenVidu session ${sessionId} was not found for closure:`, openviduError.message);
}
// Update local storage
await storage.updateSession(sessionId, {
status: 'ended',
endedAt: new Date()
});
logger.info(`Session closed: ${sessionId}`, {
clientId: req.auth.clientId,
requestId: req.context?.requestId
});
res.json({
success: true,
message: 'Session closed successfully',
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error closing session:', error);
next(error);
}
}
);
/**
* POST /api/sessions/:sessionId/force-disconnect/:connectionId
* Force disconnect a connection from a session
*/
router.post('/:sessionId/force-disconnect/:connectionId',
authenticateToken,
[
param('sessionId').isString().isLength({ min: 1 }),
param('connectionId').isString().isLength({ min: 1 })
],
handleValidationErrors,
async (req, res, next) => {
try {
const { sessionId, connectionId } = req.params;
// This would require additional OpenVidu API calls
// For now, we'll just update our local storage
const sessionData = await storage.getSession(sessionId);
if (!sessionData) {
return res.status(404).json({
error: 'Not Found',
message: 'Session not found',
requestId: req.context?.requestId
});
}
// Remove connection from session data
if (sessionData.connections) {
sessionData.connections = sessionData.connections.filter(
conn => conn.connectionId !== connectionId
);
await storage.updateSession(sessionId, { connections: sessionData.connections });
}
logger.info(`Connection ${connectionId} disconnected from session ${sessionId}`, {
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;
};