UNPKG

aethercall

Version:

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

372 lines (335 loc) 13.7 kB
/** * 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; };