import { MeetRoom, UserRole } from '@typings-ce'; import { NextFunction, Request, Response } from 'express'; import { container } from '../config/index.js'; import { RecordingHelper } from '../helpers/index.js'; import { errorInsufficientPermissions, errorInvalidRecordingSecret, errorRecordingDisabled, errorRecordingNotFound, handleError, rejectRequestFromMeetError } from '../models/error.model.js'; import { LoggerService, MeetStorageService, ParticipantService, RoomService } from '../services/index.js'; import { allowAnonymous, apiKeyValidator, recordingTokenValidator, tokenAndRoleValidator, withAuth } from './auth.middleware.js'; export const withRecordingEnabled = async (req: Request, res: Response, next: NextFunction) => { const logger = container.get(LoggerService); const roomService = container.get(RoomService); try { const roomId = extractRoomIdFromRequest(req); const room: MeetRoom = await roomService.getMeetRoom(roomId!); if (!room.preferences?.recordingPreferences?.enabled) { logger.debug(`Recording is disabled for room '${roomId}'`); const error = errorRecordingDisabled(roomId!); return rejectRequestFromMeetError(res, error); } return next(); } catch (error) { handleError(res, error, 'checking recording preferences'); } }; export const withCanRecordPermission = async (req: Request, res: Response, next: NextFunction) => { const roomId = extractRoomIdFromRequest(req); const payload = req.session?.tokenClaims; const role = req.session?.participantRole; if (!payload || !role) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } const participantService = container.get(ParticipantService); const metadata = participantService.parseMetadata(payload.metadata || '{}'); const sameRoom = payload.video?.room === roomId; const permissions = metadata.roles.find((r) => r.role === role)?.permissions; const canRecord = permissions?.canRecord; if (!sameRoom || !canRecord) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } return next(); }; export const withCanRetrieveRecordingsPermission = async (req: Request, res: Response, next: NextFunction) => { const roomId = extractRoomIdFromRequest(req); const payload = req.session?.tokenClaims; /** * If there is no token, the user is allowed to access the resource because one of the following reasons: * * - The request is invoked using the API key. * - The user is admin. * - The user is anonymous and is using the public access secret. * - The user is using the private access secret and is authenticated. */ if (!payload) { return next(); } const roomService = container.get(RoomService); const metadata = roomService.parseRecordingTokenMetadata(payload.metadata || '{}'); const sameRoom = roomId ? payload.video?.room === roomId : true; const canRetrieveRecordings = metadata.recordingPermissions.canRetrieveRecordings; if (!sameRoom || !canRetrieveRecordings) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } return next(); }; export const withCanDeleteRecordingsPermission = async (req: Request, res: Response, next: NextFunction) => { const roomId = extractRoomIdFromRequest(req); const payload = req.session?.tokenClaims; // If there is no token, the user is admin or it is invoked using the API key // In this case, the user is allowed to access the resource if (!payload) { return next(); } const roomService = container.get(RoomService); const metadata = roomService.parseRecordingTokenMetadata(payload.metadata || '{}'); const sameRoom = roomId ? payload.video?.room === roomId : true; const canDeleteRecordings = metadata.recordingPermissions.canDeleteRecordings; if (!sameRoom || !canDeleteRecordings) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } return next(); }; /** * Middleware to configure authentication for retrieving recording based on the provided secret. * * - If a valid secret is provided in the query, access is granted according to the secret type. * - If no secret is provided, the default authentication logic is applied, i.e., API key, admin and recording token access. */ export const configureRecordingAuth = async (req: Request, res: Response, next: NextFunction) => { const storageService = container.get(MeetStorageService); const secret = req.query.secret as string; // If a secret is provided, validate it against the stored secrets // and apply the appropriate authentication logic. if (secret) { try { const recordingId = req.params.recordingId as string; const recordingSecrets = await storageService.getAccessRecordingSecrets(recordingId); if (!recordingSecrets) { const error = errorRecordingNotFound(recordingId); return rejectRequestFromMeetError(res, error); } const authValidators = []; switch (secret) { case recordingSecrets.publicAccessSecret: // Public access secret allows anonymous access authValidators.push(allowAnonymous); break; case recordingSecrets.privateAccessSecret: // Private access secret requires authentication with user role authValidators.push(tokenAndRoleValidator(UserRole.USER)); break; default: // Invalid secret provided return rejectRequestFromMeetError(res, errorInvalidRecordingSecret(recordingId, secret)); } return withAuth(...authValidators)(req, res, next); } catch (error) { return handleError(res, error, 'retrieving recording secrets'); } } // If no secret is provided, we proceed with the default authentication logic. // This will allow API key, admin and recording token access. const authValidators = [apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator]; return withAuth(...authValidators)(req, res, next); }; const extractRoomIdFromRequest = (req: Request): string | undefined => { if (req.body.roomId) { return req.body.roomId as string; } // If roomId is not in the body, check if it's in the params const recordingId = req.params.recordingId as string; if (!recordingId) { return undefined; } const { roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId); return roomId; };