aethercall
Version:
A scalable WebRTC video calling API built with Node.js and OpenVidu
372 lines (335 loc) • 13.7 kB
JavaScript
/**
* Authentication Routes
* HTTP endpoints for authentication and authorization
*/
const express = require('express');
const { body, validationResult } = require('express-validator');
const logger = require('../../../utils/logger');
module.exports = (dependencies) => {
const router = express.Router();
const { 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();
};
/**
* POST /api/auth/token
* Generate API access token
*/
router.post('/token',
[
body('clientId').isString().isLength({ min: 1 }),
body('clientSecret').optional().isString(),
body('scopes').optional().isArray(),
body('expiresIn').optional().isString()
],
handleValidationErrors,
async (req, res, next) => {
try {
const { clientId, clientSecret, scopes = ['sessions:read', 'sessions:write'], expiresIn } = req.body;
// In a real implementation, you would validate clientId and clientSecret
// against a database of registered applications
// For now, we'll allow any clientId for demonstration
const token = tokenManager.generateAPIToken(clientId, scopes, {
clientSecret: clientSecret ? 'provided' : 'none',
userAgent: req.get('User-Agent'),
ip: req.ip
});
logger.info(`API token generated for client: ${clientId}`, {
scopes: scopes,
requestId: req.context?.requestId
});
res.json({
success: true,
data: {
accessToken: token,
tokenType: 'Bearer',
expiresIn: expiresIn || '24h',
scopes: scopes
},
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error generating API token:', error);
next(error);
}
}
);
/**
* POST /api/auth/room
* Create a room with access code
*/
router.post('/room',
[
body('roomName').optional().isString().isLength({ min: 1, max: 100 }),
body('maxParticipants').optional().isInt({ min: 1, max: 100 }),
body('recordingMode').optional().isIn(['ALWAYS', 'MANUAL', 'NEVER']),
body('requireAuth').optional().isBoolean(),
body('password').optional().isString(),
body('metadata').optional().isObject()
],
handleValidationErrors,
async (req, res, next) => {
try {
const {
roomName,
maxParticipants = 10,
recordingMode = 'MANUAL',
requireAuth = false,
password,
metadata = {}
} = req.body;
// Generate room code
const roomCode = tokenManager.generateRoomCode(8);
// Generate room ID (can be used as session ID)
const roomId = tokenManager.generateSecureRandom(16);
// Store room data
const roomData = {
roomId: roomId,
roomCode: roomCode,
roomName: roomName || `Room ${roomCode}`,
maxParticipants: maxParticipants,
recordingMode: recordingMode,
requireAuth: requireAuth,
password: password,
metadata: {
...metadata,
roomCode: roomCode
},
status: 'waiting', // waiting, active, ended
participants: [],
createdAt: new Date(),
createdBy: req.ip
};
// Store as session (room is essentially a session)
await storage.storeSession(roomId, roomData);
logger.info(`Room created: ${roomCode}`, {
roomId: roomId,
roomName: roomData.roomName,
requestId: req.context?.requestId
});
res.status(201).json({
success: true,
data: {
roomId: roomId,
roomCode: roomCode,
roomName: roomData.roomName,
maxParticipants: maxParticipants,
recordingMode: recordingMode,
requireAuth: requireAuth,
hasPassword: !!password,
status: 'waiting',
createdAt: roomData.createdAt
},
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error creating room:', error);
next(error);
}
}
);
/**
* POST /api/auth/room/join
* Join a room with room code and optional password
*/
router.post('/room/join',
[
body('roomCode').isString().isLength({ min: 1 }),
body('displayName').isString().isLength({ min: 1, max: 50 }),
body('password').optional().isString(),
body('userId').optional().isString()
],
handleValidationErrors,
async (req, res, next) => {
try {
const { roomCode, displayName, password, userId } = req.body;
// Find room by code
const sessions = await storage.getActiveSessions();
const room = sessions.find(s =>
s.metadata && s.metadata.roomCode === roomCode
);
if (!room) {
return res.status(404).json({
error: 'Not Found',
message: 'Room not found',
requestId: req.context?.requestId
});
}
// Check if room has ended
if (room.status === 'ended') {
return res.status(400).json({
error: 'Bad Request',
message: 'Room has ended',
requestId: req.context?.requestId
});
}
// Check password if required
if (room.password && room.password !== password) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid room password',
requestId: req.context?.requestId
});
}
// Check participant limit
const currentParticipants = room.connections ? room.connections.length : 0;
if (currentParticipants >= room.maxParticipants) {
return res.status(400).json({
error: 'Bad Request',
message: 'Room is full',
requestId: req.context?.requestId
});
}
// Generate user ID if not provided
const participantId = userId || tokenManager.generateSecureRandom(8);
// Generate guest token for room access
const guestToken = tokenManager.generateSessionToken(
room.roomId,
participantId,
'PUBLISHER',
{
displayName,
roomCode,
joinedAt: new Date()
}
);
logger.info(`User joining room: ${roomCode}`, {
participantId: participantId,
displayName: displayName,
roomId: room.roomId,
requestId: req.context?.requestId
});
res.json({
success: true,
data: {
roomId: room.roomId,
roomCode: roomCode,
roomName: room.roomName,
participantId: participantId,
displayName: displayName,
guestToken: guestToken,
requiresConnection: true,
message: 'Room access granted. Use the connection API to get your connection token.'
},
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error joining room:', error);
next(error);
}
}
);
/**
* GET /api/auth/room/:roomCode
* Get room information
*/
router.get('/room/:roomCode',
async (req, res, next) => {
try {
const roomCode = req.params.roomCode;
// Find room by code
const sessions = await storage.getActiveSessions();
const room = sessions.find(s =>
s.metadata && s.metadata.roomCode === roomCode
);
if (!room) {
return res.status(404).json({
error: 'Not Found',
message: 'Room not found',
requestId: req.context?.requestId
});
}
const participantCount = room.connections ? room.connections.length : 0;
res.json({
success: true,
data: {
roomCode: roomCode,
roomName: room.roomName,
status: room.status,
participantCount: participantCount,
maxParticipants: room.maxParticipants,
hasPassword: !!room.password,
requireAuth: room.requireAuth,
recordingMode: room.recordingMode,
createdAt: room.createdAt
},
requestId: req.context?.requestId
});
} catch (error) {
logger.error('Error fetching room info:', error);
next(error);
}
}
);
/**
* POST /api/auth/verify
* Verify a token (API or Session token)
*/
router.post('/verify',
[
body('token').isString().isLength({ min: 1 }),
body('tokenType').optional().isIn(['api', 'session'])
],
handleValidationErrors,
async (req, res, next) => {
try {
const { token, tokenType } = req.body;
let tokenData;
let type;
try {
if (tokenType === 'session') {
tokenData = tokenManager.validateSessionToken(token);
type = 'session';
} else if (tokenType === 'api') {
tokenData = tokenManager.validateAPIToken(token);
type = 'api';
} else {
// Auto-detect token type
const payload = tokenManager.verifyJWT(token);
type = payload.type;
if (type === 'session') {
tokenData = tokenManager.validateSessionToken(token);
} else if (type === 'api') {
tokenData = tokenManager.validateAPIToken(token);
} else {
throw new Error('Unknown token type');
}
}
res.json({
success: true,
data: {
valid: true,
type: type,
...tokenData
},
requestId: req.context?.requestId
});
} catch (error) {
res.status(400).json({
success: false,
data: {
valid: false,
error: error.message
},
requestId: req.context?.requestId
});
}
} catch (error) {
logger.error('Error verifying token:', error);
next(error);
}
}
);
return router;
};