backend: refactor room member and user authentication middleware, adding new permissions and request handling

This commit is contained in:
juancarmore 2025-12-09 21:07:18 +01:00
parent b709ad320a
commit 79cef519b8
10 changed files with 371 additions and 225 deletions

View File

@ -1,5 +1,7 @@
import { MeetRoom, MeetRoomOptions } from '@openvidu-meet/typings';
import { Request } from 'express';
import { MEET_ENV } from '../environment.js';
import { RecordingHelper } from './recording.helper.js';
export class MeetRoomHelper {
private constructor() {
@ -81,7 +83,8 @@ export class MeetRoomHelper {
* - moderatorSecret: The secret extracted from the moderator room URL
*/
static extractSecretsFromRoom(room: MeetRoom): { speakerSecret: string; moderatorSecret: string } {
const { speakerUrl, moderatorUrl } = room;
const speakerUrl = room.anonymous.speaker.accessUrl;
const moderatorUrl = room.anonymous.moderator.accessUrl;
const parsedSpeakerUrl = new URL(speakerUrl);
const speakerSecret = parsedSpeakerUrl.searchParams.get('secret') || '';
@ -105,4 +108,36 @@ export class MeetRoomHelper {
return false;
}
}
/**
* Extracts the room ID from the request object.
* It checks the following locations in order:
* 1. req.params.roomId
* 2. req.body.roomId
* 3. req.params.recordingId (extracts roomId from it)
*
* @param req - The express request object
* @returns The extracted room ID or undefined if not found
*/
static getRoomIdFromRequest(req: Request): string | undefined {
// 1. Check params
if (req.params.roomId) {
return req.params.roomId;
}
// 2. Check body
if (req.body.roomId) {
return req.body.roomId;
}
// 3. Check recordingId in params
const recordingId = req.params.recordingId;
if (recordingId) {
const { roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId);
return roomId;
}
return undefined;
}
}

View File

@ -1,78 +0,0 @@
import { AuthMode, MeetRoomMemberRole, MeetRoomMemberTokenOptions, MeetUserRole } from '@openvidu-meet/typings';
import { NextFunction, Request, Response } from 'express';
import { container } from '../config/dependency-injector.config.js';
import { errorInsufficientPermissions, handleError, rejectRequestFromMeetError } from '../models/error.model.js';
import { GlobalConfigService } from '../services/global-config.service.js';
import { RequestSessionService } from '../services/request-session.service.js';
import { RoomMemberService } from '../services/room-member.service.js';
import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
/**
* Middleware to configure authentication for generating token to access room and its resources
* based on room member role and authentication mode.
*
* - If the authentication mode is MODERATORS_ONLY and the room member role is MODERATOR, configure user authentication.
* - If the authentication mode is ALL_USERS, configure user authentication.
* - Otherwise, allow anonymous access.
*/
export const configureRoomMemberTokenAuth = async (req: Request, res: Response, next: NextFunction) => {
const configService = container.get(GlobalConfigService);
const roomMemberService = container.get(RoomMemberService);
let role: MeetRoomMemberRole;
try {
const { roomId } = req.params;
const { secret } = req.body as MeetRoomMemberTokenOptions;
role = await roomMemberService.getRoomMemberRoleBySecret(roomId, secret);
} catch (error) {
return handleError(res, error, 'getting room member role by secret');
}
let authModeToAccessRoom: AuthMode;
try {
const securityConfig = await configService.getSecurityConfig();
authModeToAccessRoom = securityConfig.authentication.authModeToAccessRoom;
} catch (error) {
return handleError(res, error, 'checking authentication config');
}
const authValidators = [];
if (authModeToAccessRoom === AuthMode.NONE) {
authValidators.push(allowAnonymous);
} else {
const isModeratorsOnlyMode =
authModeToAccessRoom === AuthMode.MODERATORS_ONLY && role === MeetRoomMemberRole.MODERATOR;
const isAllUsersMode = authModeToAccessRoom === AuthMode.ALL_USERS;
if (isModeratorsOnlyMode || isAllUsersMode) {
authValidators.push(tokenAndRoleValidator(MeetUserRole.USER));
} else {
authValidators.push(allowAnonymous);
}
}
return withAuth(...authValidators)(req, res, next);
};
export const withModeratorPermissions = async (req: Request, res: Response, next: NextFunction) => {
const { roomId } = req.params;
const requestSessionService = container.get(RequestSessionService);
const tokenRoomId = requestSessionService.getRoomIdFromToken();
const role = requestSessionService.getRoomMemberRole();
if (!tokenRoomId || !role) {
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
if (tokenRoomId !== roomId || role !== MeetRoomMemberRole.MODERATOR) {
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
return next();
};

View File

@ -1,7 +1,7 @@
import { MeetRoom, MeetUserRole } from '@openvidu-meet/typings';
import { MeetRoomMemberPermissions, MeetUserRole } from '@openvidu-meet/typings';
import { NextFunction, Request, Response } from 'express';
import { container } from '../config/dependency-injector.config.js';
import { RecordingHelper } from '../helpers/recording.helper.js';
import { MeetRoomHelper } from '../helpers/room.helper.js';
import {
errorInsufficientPermissions,
errorInvalidRecordingSecret,
@ -26,8 +26,8 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne
const roomService = container.get(RoomService);
try {
const roomId = extractRoomIdFromRequest(req);
const room: MeetRoom = await roomService.getMeetRoom(roomId!);
const roomId = MeetRoomHelper.getRoomIdFromRequest(req);
const room = await roomService.getMeetRoom(roomId!);
if (!room.config.recording.enabled) {
logger.debug(`Recording is disabled for room '${roomId}'`);
@ -41,97 +41,13 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne
}
};
export const withCanRecordPermission = async (req: Request, res: Response, next: NextFunction) => {
const roomId = extractRoomIdFromRequest(req);
const requestSessionService = container.get(RequestSessionService);
const tokenRoomId = requestSessionService.getRoomIdFromToken();
const permissions = requestSessionService.getRoomMemberMeetPermissions();
if (!tokenRoomId || !permissions) {
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
if (tokenRoomId !== roomId || !permissions.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 requestSessionService = container.get(RequestSessionService);
const tokenRoomId = requestSessionService.getRoomIdFromToken();
/**
* 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 (!tokenRoomId) {
return next();
}
const permissions = requestSessionService.getRoomMemberMeetPermissions();
if (!permissions) {
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
const sameRoom = roomId ? tokenRoomId === roomId : true;
if (!sameRoom || !permissions.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 requestSessionService = container.get(RequestSessionService);
const tokenRoomId = requestSessionService.getRoomIdFromToken();
// 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 (!tokenRoomId) {
return next();
}
const permissions = requestSessionService.getRoomMemberMeetPermissions();
if (!permissions) {
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
const sameRoom = roomId ? tokenRoomId === roomId : true;
if (!sameRoom || !permissions.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 room member token access.
*/
export const configureRecordingAuth = async (req: Request, res: Response, next: NextFunction) => {
export const setupRecordingAuthentication = async (req: Request, res: Response, next: NextFunction) => {
const secret = req.query.secret as string;
// If a secret is provided, validate it against the stored secrets
@ -151,8 +67,10 @@ export const configureRecordingAuth = async (req: Request, res: Response, next:
authValidators.push(allowAnonymous);
break;
case recordingSecrets.privateAccessSecret:
// Private access secret requires authentication with user role
authValidators.push(tokenAndRoleValidator(MeetUserRole.USER));
// Private access secret requires authentication
authValidators.push(
tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER)
);
break;
default:
// Invalid secret provided
@ -166,23 +84,96 @@ export const configureRecordingAuth = async (req: Request, res: Response, next:
}
// If no secret is provided, we proceed with the default authentication logic.
// This will allow API key, admin and room member token access.
const authValidators = [apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator];
// This will allow API key, registered user and room member token access.
const authValidators = [
apiKeyValidator,
tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER),
roomMemberTokenValidator
];
return withAuth(...authValidators)(req, res, next);
};
const extractRoomIdFromRequest = (req: Request): string | undefined => {
if (req.body.roomId) {
return req.body.roomId as string;
}
/**
* Middleware to authorize recording access (retrieval or deletion).
*
* - If a secret is provided in the request query, and allowSecretAccess is true,
* it assumes the secret has been validated and grants access.
* - If a Room Member Token is used, it checks that the token's roomId matches the requested roomId
* and that the member has the required permission.
* - If a registered user is authenticated, it checks their role and whether they are the owner or a member of the room
* with the required permission.
* - If neither a valid token nor an authenticated user is present, it rejects the request.
*
* @param permission - The permission to check (canRetrieveRecordings or canDeleteRecordings).
* @param allowSecretAccess - Whether to allow access based on a valid secret in the query.
*/
export const authorizeRecordingAccess = (permission: keyof MeetRoomMemberPermissions, allowSecretAccess = false) => {
return async (req: Request, res: Response, next: NextFunction) => {
const roomId = MeetRoomHelper.getRoomIdFromRequest(req);
const secret = req.query.secret as string;
// If roomId is not in the body, check if it's in the params
const recordingId = req.params.recordingId as string;
// If allowSecretAccess is true and a secret is provided,
// we assume it has been validated by setupRecordingAuthentication.
if (allowSecretAccess && secret) {
return next();
}
if (!recordingId) {
return undefined;
}
const requestSessionService = container.get(RequestSessionService);
const roomService = container.get(RoomService);
const { roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId);
return roomId;
const memberRoomId = requestSessionService.getRoomIdFromMember();
const user = requestSessionService.getAuthenticatedUser();
const forbiddenError = errorInsufficientPermissions();
// Case 1: Room Member Token
if (memberRoomId) {
const permissions = requestSessionService.getRoomMemberPermissions();
if (!permissions) {
return rejectRequestFromMeetError(res, forbiddenError);
}
const sameRoom = roomId ? memberRoomId === roomId : true;
if (!sameRoom || !permissions[permission]) {
return rejectRequestFromMeetError(res, forbiddenError);
}
return next();
}
// Case 2: Authenticated User
if (user) {
// If no roomId is specified, we are in a listing/bulk request
// Each recording's room ownership and permissions will be checked individually
if (!roomId) {
return next();
}
// Admins can always access
if (user.role === MeetUserRole.ADMIN) {
return next();
}
// Check if owner
const isOwner = await roomService.isRoomOwner(roomId, user.userId);
if (isOwner) {
return next();
}
// Check if member with permissions
const member = await roomService.getRoomMember(roomId, user.userId);
if (member && member.effectivePermissions[permission]) {
return next();
}
return rejectRequestFromMeetError(res, forbiddenError);
}
// Otherwise, reject the request
return rejectRequestFromMeetError(res, forbiddenError);
};
};

View File

@ -0,0 +1,103 @@
import { MeetRoomMemberPermissions, MeetRoomMemberTokenOptions, MeetUserRole } from '@openvidu-meet/typings';
import { NextFunction, Request, Response } from 'express';
import { container } from '../config/dependency-injector.config.js';
import { MeetRoomHelper } from '../helpers/room.helper.js';
import { errorInsufficientPermissions, rejectRequestFromMeetError } from '../models/error.model.js';
import { RequestSessionService } from '../services/request-session.service.js';
import { RoomService } from '../services/room.service.js';
import { allowAnonymous, AuthValidator, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
/**
* Middleware to configure authentication for generating room member tokens.
*
* - If a secret is provided in the request body, anonymous access is allowed.
* - If no secret is provided, the user must be authenticated as ADMIN, USER, or ROOM_MEMBER.
*/
export const setupRoomMemberTokenAuthentication = async (req: Request, res: Response, next: NextFunction) => {
const { secret } = req.body as MeetRoomMemberTokenOptions;
const authValidators: AuthValidator[] = [];
if (secret) {
authValidators.push(allowAnonymous);
} else {
authValidators.push(tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER));
}
return withAuth(...authValidators)(req, res, next);
};
/**
* Middleware to authorize the generation of a room member token.
*
* - If a secret is provided, it checks if it matches a valid room secret (anonymous access) or if it corresponds to a room member.
* - If no secret is provided, it checks if the authenticated user has permissions to access the room (Admin, Owner, or Member).
*/
export const authorizeRoomMemberTokenGeneration = async (req: Request, res: Response, next: NextFunction) => {
const { roomId } = req.params;
const { secret } = req.body as MeetRoomMemberTokenOptions;
const requestSessionService = container.get(RequestSessionService);
const roomService = container.get(RoomService);
const user = requestSessionService.getAuthenticatedUser();
const forbiddenError = errorInsufficientPermissions();
// Scenario 1: Secret provided (Anonymous access or Member ID)
if (secret) {
// Check if secret matches any room access URL secret
const isValidSecret = await roomService.isValidRoomSecret(roomId, secret);
if (isValidSecret) {
return next();
}
// Check if secret is a memberId
const isMember = await roomService.isRoomMember(roomId, secret);
if (isMember) {
return next();
}
return rejectRequestFromMeetError(res, forbiddenError);
}
// Scenario 2: No secret provided (Authenticated User)
if (user) {
const canAccess = await roomService.canUserAccessRoom(roomId, user);
if (!canAccess) {
return rejectRequestFromMeetError(res, forbiddenError);
}
return next();
}
return rejectRequestFromMeetError(res, forbiddenError);
};
/**
* Middleware to check if the room member has a specific permission.
*
* @param permission The permission to check (key of MeetRoomMemberPermissions).
*/
export const withRoomMemberPermission = (permission: keyof MeetRoomMemberPermissions) => {
return async (req: Request, res: Response, next: NextFunction) => {
const roomId = MeetRoomHelper.getRoomIdFromRequest(req);
const requestSessionService = container.get(RequestSessionService);
const memberRoomId = requestSessionService.getRoomIdFromMember();
const permissions = requestSessionService.getRoomMemberPermissions();
if (!memberRoomId || !permissions) {
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
if (memberRoomId !== roomId || !permissions[permission]) {
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
}
return next();
};
};

View File

@ -2,31 +2,46 @@ import { NextFunction, Request, Response } from 'express';
import { container } from '../config/dependency-injector.config.js';
import { errorInsufficientPermissions, rejectRequestFromMeetError } from '../models/error.model.js';
import { RequestSessionService } from '../services/request-session.service.js';
import { RoomService } from '../services/room.service.js';
/**
* Middleware that configures authorization for accessing a specific room.
* Middleware to authorize access to a room.
*
* - If there is no token in the session, the user is granted access (admin or API key).
* - If the user does not belong to the requested room, access is denied.
* - Otherwise, the user is allowed to access the room.
* - If a Room Member Token is used, it checks that the token's roomId matches the requested roomId.
* - If a registered user is authenticated, it checks their role and whether they are the owner or a member of the room.
* - If neither a valid token nor an authenticated user is present, it rejects the request.
*/
export const configureRoomAuthorization = async (req: Request, res: Response, next: NextFunction) => {
export const authorizeRoomAccess = async (req: Request, res: Response, next: NextFunction) => {
const roomId = req.params.roomId as string;
const requestSessionService = container.get(RequestSessionService);
const tokenRoomId = requestSessionService.getRoomIdFromToken();
const memberRoomId = requestSessionService.getRoomIdFromMember();
const user = requestSessionService.getAuthenticatedUser();
const forbiddenError = errorInsufficientPermissions();
// Room Member Token
if (memberRoomId) {
// Check if the member's roomId matches the requested roomId
if (memberRoomId !== roomId) {
return rejectRequestFromMeetError(res, forbiddenError);
}
// 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 (!tokenRoomId) {
return next();
}
// If the user does not belong to the requested room, access is denied
if (tokenRoomId !== roomId) {
const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error);
// Registered User
if (user) {
const roomService = container.get(RoomService);
const canAccess = await roomService.canUserAccessRoom(roomId, user);
if (!canAccess) {
return rejectRequestFromMeetError(res, forbiddenError);
}
return next();
}
return next();
// If there is no token and no user, reject the request
return rejectRequestFromMeetError(res, forbiddenError);
};

View File

@ -2,7 +2,7 @@ import bodyParser from 'body-parser';
import { Router } from 'express';
import * as meetingCtrl from '../controllers/meeting.controller.js';
import { roomMemberTokenValidator, withAuth } from '../middlewares/auth.middleware.js';
import { withModeratorPermissions } from '../middlewares/room-member.middleware.js';
import { withRoomMemberPermission } from '../middlewares/room-member.middleware.js';
import { validateUpdateParticipantRoleReq } from '../middlewares/request-validators/meeting-validator.middleware.js';
import { withValidRoomId } from '../middlewares/request-validators/room-validator.middleware.js';
@ -15,21 +15,21 @@ internalMeetingRouter.delete(
'/:roomId',
withAuth(roomMemberTokenValidator),
withValidRoomId,
withModeratorPermissions,
withRoomMemberPermission('canEndMeeting'),
meetingCtrl.endMeeting
);
internalMeetingRouter.delete(
'/:roomId/participants/:participantIdentity',
withAuth(roomMemberTokenValidator),
withValidRoomId,
withModeratorPermissions,
withRoomMemberPermission('canKickParticipants'),
meetingCtrl.kickParticipantFromMeeting
);
internalMeetingRouter.put(
'/:roomId/participants/:participantIdentity/role',
withAuth(roomMemberTokenValidator),
withValidRoomId,
withModeratorPermissions,
withRoomMemberPermission('canMakeModerator'),
validateUpdateParticipantRoleReq,
meetingCtrl.updateParticipantRole
);

View File

@ -9,10 +9,8 @@ import {
withAuth
} from '../middlewares/auth.middleware.js';
import {
configureRecordingAuth,
withCanDeleteRecordingsPermission,
withCanRecordPermission,
withCanRetrieveRecordingsPermission,
authorizeRecordingAccess,
setupRecordingAuthentication,
withRecordingEnabled
} from '../middlewares/recording.middleware.js';
import {
@ -24,6 +22,7 @@ import {
validateStartRecordingReq,
withValidRecordingId
} from '../middlewares/request-validators/recording-validator.middleware.js';
import { withRoomMemberPermission } from '../middlewares/room-member.middleware.js';
export const recordingRouter: Router = Router();
recordingRouter.use(bodyParser.urlencoded({ extended: true }));
@ -37,8 +36,8 @@ recordingRouter.get(
tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER),
roomMemberTokenValidator
),
withCanRetrieveRecordingsPermission,
validateGetRecordingsReq,
authorizeRecordingAccess('canRetrieveRecordings'),
recordingCtrl.getRecordings
);
recordingRouter.delete(
@ -49,7 +48,7 @@ recordingRouter.delete(
roomMemberTokenValidator
),
validateBulkDeleteRecordingsReq,
withCanDeleteRecordingsPermission,
authorizeRecordingAccess('canDeleteRecordings'),
recordingCtrl.bulkDeleteRecordings
);
recordingRouter.get(
@ -60,14 +59,14 @@ recordingRouter.get(
roomMemberTokenValidator
),
validateBulkDeleteRecordingsReq,
withCanRetrieveRecordingsPermission,
authorizeRecordingAccess('canRetrieveRecordings'),
recordingCtrl.downloadRecordingsZip
);
recordingRouter.get(
'/:recordingId',
validateGetRecordingReq,
configureRecordingAuth,
withCanRetrieveRecordingsPermission,
setupRecordingAuthentication,
authorizeRecordingAccess('canRetrieveRecordings', true),
recordingCtrl.getRecording
);
recordingRouter.delete(
@ -78,14 +77,14 @@ recordingRouter.delete(
roomMemberTokenValidator
),
withValidRecordingId,
withCanDeleteRecordingsPermission,
authorizeRecordingAccess('canDeleteRecordings'),
recordingCtrl.deleteRecording
);
recordingRouter.get(
'/:recordingId/media',
validateGetRecordingMediaReq,
configureRecordingAuth,
withCanRetrieveRecordingsPermission,
setupRecordingAuthentication,
authorizeRecordingAccess('canRetrieveRecordings', true),
recordingCtrl.getRecordingMedia
);
recordingRouter.get(
@ -96,7 +95,7 @@ recordingRouter.get(
roomMemberTokenValidator
),
validateGetRecordingUrlReq,
withCanRetrieveRecordingsPermission,
authorizeRecordingAccess('canRetrieveRecordings'),
recordingCtrl.getRecordingUrl
);
@ -110,7 +109,7 @@ internalRecordingRouter.post(
validateStartRecordingReq,
withRecordingEnabled,
withAuth(roomMemberTokenValidator),
withCanRecordPermission,
withRoomMemberPermission('canRecord'),
recordingCtrl.startRecording
);
internalRecordingRouter.post(
@ -118,6 +117,6 @@ internalRecordingRouter.post(
withValidRecordingId,
withRecordingEnabled,
withAuth(roomMemberTokenValidator),
withCanRecordPermission,
withRoomMemberPermission('canRecord'),
recordingCtrl.stopRecording
);

View File

@ -9,7 +9,6 @@ import {
tokenAndRoleValidator,
withAuth
} from '../middlewares/auth.middleware.js';
import { configureRoomMemberTokenAuth } from '../middlewares/room-member.middleware.js';
import {
validateBulkDeleteRoomMembersReq,
validateCreateRoomMemberReq,
@ -28,7 +27,11 @@ import {
validateUpdateRoomStatusReq,
withValidRoomId
} from '../middlewares/request-validators/room-validator.middleware.js';
import { configureRoomAuthorization } from '../middlewares/room.middleware.js';
import {
authorizeRoomMemberTokenGeneration,
setupRoomMemberTokenAuthentication
} from '../middlewares/room-member.middleware.js';
import { authorizeRoomAccess } from '../middlewares/room.middleware.js';
export const roomRouter: Router = Router();
roomRouter.use(bodyParser.urlencoded({ extended: true }));
@ -62,7 +65,7 @@ roomRouter.get(
roomMemberTokenValidator
),
withValidRoomId,
configureRoomAuthorization,
authorizeRoomAccess,
roomCtrl.getRoom
);
roomRouter.delete(
@ -80,7 +83,7 @@ roomRouter.get(
roomMemberTokenValidator
),
withValidRoomId,
configureRoomAuthorization,
authorizeRoomAccess,
roomCtrl.getRoomConfig
);
roomRouter.put(
@ -123,7 +126,7 @@ roomRouter.post(
);
roomRouter.get(
'/:roomId/members',
withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER), roomMemberTokenValidator),
withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER)),
withValidRoomId,
validateGetRoomMembersReq,
roomMemberCtrl.getRoomMembers
@ -169,6 +172,7 @@ internalRoomRouter.post(
'/:roomId/members/token',
withValidRoomId,
validateCreateRoomMemberTokenReq,
configureRoomMemberTokenAuth,
setupRoomMemberTokenAuthentication,
authorizeRoomMemberTokenGeneration,
roomMemberCtrl.generateRoomMemberToken
);

View File

@ -2,7 +2,8 @@ import {
MeetRoomMemberPermissions,
MeetRoomMemberRole,
MeetRoomMemberTokenMetadata,
MeetUser
MeetUser,
MeetUserRole
} from '@openvidu-meet/typings';
import { AsyncLocalStorage } from 'async_hooks';
import { injectable } from 'inversify';
@ -75,6 +76,13 @@ export class RequestSessionService {
return this.getContext()?.user;
}
/**
* Gets the authenticated user's role from the current request context.
*/
getAuthenticatedUserRole(): MeetUserRole | undefined {
return this.getContext()?.user?.role;
}
/**
* Sets the room member token metadata (room ID, base role, permissions)
* in the current request context.

View File

@ -8,9 +8,12 @@ import {
MeetRoomDeletionPolicyWithRecordings,
MeetRoomDeletionSuccessCode,
MeetRoomFilters,
MeetRoomMember,
MeetRoomMemberRole,
MeetRoomOptions,
MeetRoomStatus
MeetRoomStatus,
MeetUser,
MeetUserRole
} from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { CreateOptions, Room } from 'livekit-server-sdk';
@ -233,7 +236,7 @@ export class RoomService {
}
// Remove moderatorUrl if the room member is a speaker to prevent access to moderator links
const role = this.requestSessionService.getRoomMemberRole();
const role = this.requestSessionService.getRoomMemberBaseRole();
if (role === MeetRoomMemberRole.SPEAKER) {
delete (room as Partial<MeetRoom>).moderatorUrl;
@ -634,4 +637,70 @@ export class RoomService {
);
return { successful, failed };
}
async isRoomOwner(roomId: string, userId: string): Promise<boolean> {
// TODO: Implement
return false;
}
async isRoomMember(roomId: string, memberId: string): Promise<boolean> {
// TODO: Implement
return false;
}
async getRoomMember(roomId: string, userId: string): Promise<MeetRoomMember | null> {
// TODO: Implement
return null;
}
async isValidRoomSecret(roomId: string, secret: string): Promise<boolean> {
// TODO: Implement
return false;
}
/**
* Checks if a registered user can access a specific room based on their role.
*
* @param roomId The ID of the room to check access for.
* @param user The user object containing user details and role.
* @returns A promise that resolves to true if the user can access the room, false otherwise.
*/
async canUserAccessRoom(roomId: string, user: MeetUser): Promise<boolean> {
switch (user.role) {
case MeetUserRole.ADMIN:
// Admins can access all rooms
return true;
case MeetUserRole.USER: {
// Users can access rooms they own or are members of
const isOwner = await this.isRoomOwner(roomId, user.userId);
if (isOwner) {
return true;
}
const isMember = await this.isRoomMember(roomId, user.userId);
if (isMember) {
return true;
}
return false;
}
case MeetUserRole.ROOM_MEMBER: {
// Room members can only access rooms they are members of
const isMember = await this.isRoomMember(roomId, user.userId);
if (isMember) {
return true;
}
return false;
}
default:
return false;
}
}
}