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