UNPKG

aethercall

Version:

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

234 lines (208 loc) 7.41 kB
/** * HTTP Server Setup * Express/Fastify server configuration with middleware */ const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const morgan = require('morgan'); const sessionRoutes = require('./routes/sessions'); const connectionRoutes = require('./routes/connections'); const recordingRoutes = require('./routes/recordings'); const authRoutes = require('./routes/auth'); const config = require('../../utils/config'); const logger = require('../../utils/logger'); class HTTPServer { constructor(dependencies = {}) { this.app = express(); this.openviduAPI = dependencies.openviduAPI; this.storage = dependencies.storage; this.tokenManager = dependencies.tokenManager; this._setupMiddleware(); this._setupRoutes(); this._setupErrorHandling(); } /** * Setup middleware * @private */ _setupMiddleware() { // Security middleware this.app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", "wss:", "ws:"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'self'"] } } })); // CORS configuration this.app.use(cors({ origin: config.corsOrigin, methods: config.corsMethods.split(','), credentials: true, optionsSuccessStatus: 200 })); // Rate limiting const limiter = rateLimit({ windowMs: config.rateLimitWindowMs, max: config.rateLimitMaxRequests, message: { error: 'Too many requests from this IP, please try again later.', retryAfter: Math.ceil(config.rateLimitWindowMs / 1000) }, standardHeaders: true, legacyHeaders: false }); this.app.use('/api/', limiter); // Logging this.app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } })); // Body parsing this.app.use(express.json({ limit: '10mb' })); this.app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Request context middleware this.app.use((req, res, next) => { req.context = { requestId: this._generateRequestId(), timestamp: new Date(), userAgent: req.get('User-Agent'), ip: req.ip || req.connection.remoteAddress }; next(); }); } /** * Setup API routes * @private */ _setupRoutes() { // Health check endpoint this.app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime(), version: process.env.npm_package_version || '1.0.0' }); }); // API documentation this.app.get('/', (req, res) => { res.json({ name: 'AetherCall API', description: 'Open source video calling API powered by OpenVidu', version: process.env.npm_package_version || '1.0.0', documentation: '/docs', endpoints: { sessions: '/api/sessions', connections: '/api/connections', recordings: '/api/recordings', auth: '/api/auth' } }); }); // API routes with dependency injection const dependencies = { openviduAPI: this.openviduAPI, storage: this.storage, tokenManager: this.tokenManager }; this.app.use('/api/auth', authRoutes(dependencies)); this.app.use('/api/sessions', sessionRoutes(dependencies)); this.app.use('/api/connections', connectionRoutes(dependencies)); this.app.use('/api/recordings', recordingRoutes(dependencies)); } /** * Setup error handling middleware * @private */ _setupErrorHandling() { // 404 handler this.app.use((req, res, next) => { res.status(404).json({ error: 'Not Found', message: `The requested endpoint ${req.originalUrl} was not found`, requestId: req.context?.requestId }); }); // Global error handler this.app.use((err, req, res, next) => { logger.error('Unhandled error:', { error: err.message, stack: err.stack, requestId: req.context?.requestId, url: req.originalUrl, method: req.method }); const statusCode = err.statusCode || err.status || 500; const message = err.message || 'Internal Server Error'; res.status(statusCode).json({ error: statusCode === 500 ? 'Internal Server Error' : message, requestId: req.context?.requestId, ...(config.nodeEnv === 'development' && { stack: err.stack }) }); }); } /** * Start the HTTP server * @param {number} port - Port to listen on * @param {string} host - Host to bind to * @returns {Promise<void>} */ async start(port = config.port, host = config.host) { return new Promise((resolve, reject) => { try { this.server = this.app.listen(port, host, () => { logger.info(`AetherCall HTTP server started on http://${host}:${port}`); resolve(); }); this.server.on('error', reject); } catch (error) { reject(error); } }); } /** * Stop the HTTP server * @returns {Promise<void>} */ async stop() { if (this.server) { return new Promise((resolve, reject) => { this.server.close((err) => { if (err) { reject(err); } else { logger.info('HTTP server stopped'); resolve(); } }); }); } } /** * Generate a unique request ID * @returns {string} Request ID * @private */ _generateRequestId() { return Math.random().toString(36).substr(2, 9) + Date.now().toString(36); } /** * Get the Express app instance * @returns {Object} Express app */ getApp() { return this.app; } } module.exports = HTTPServer;