diff --git a/meet-ce/backend/src/models/mongoose-schemas/room-member.schema.ts b/meet-ce/backend/src/models/mongoose-schemas/room-member.schema.ts index b867fea5..7b52b279 100644 --- a/meet-ce/backend/src/models/mongoose-schemas/room-member.schema.ts +++ b/meet-ce/backend/src/models/mongoose-schemas/room-member.schema.ts @@ -7,7 +7,7 @@ import { INTERNAL_CONFIG } from '../../config/internal-config.js'; * Extends the MeetRoomMember interface with MongoDB Document functionality. * Note: effectivePermissions is computed, not stored. */ -export interface MeetRoomMemberDocument extends Omit, Document { +export interface MeetRoomMemberDocument extends MeetRoomMember, Document { /** Schema version for migration tracking (internal use only) */ schemaVersion?: number; } @@ -88,6 +88,10 @@ const MeetRoomMemberSchema = new Schema( type: MeetRoomMemberPartialPermissionsSchema, required: false }, + effectivePermissions: { + type: MeetRoomMemberPermissionsSchema, + required: true + }, currentParticipantIdentity: { type: String, required: false diff --git a/meet-ce/backend/src/repositories/room-member.repository.ts b/meet-ce/backend/src/repositories/room-member.repository.ts index 20244afd..7457b260 100644 --- a/meet-ce/backend/src/repositories/room-member.repository.ts +++ b/meet-ce/backend/src/repositories/room-member.repository.ts @@ -1,51 +1,30 @@ -import { - MeetRoomMember, - MeetRoomMemberFilters, - MeetRoomMemberPermissions, - MeetRoomMemberRole, - MeetRoomRoles -} from '@openvidu-meet/typings'; +import { MeetRoomMember, MeetRoomMemberFilters, MeetRoomMemberPermissions } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; -import { errorRoomNotFound } from '../models/error.model.js'; import { MeetRoomMemberDocument, MeetRoomMemberModel } from '../models/mongoose-schemas/room-member.schema.js'; import { LoggerService } from '../services/logger.service.js'; import { BaseRepository } from './base.repository.js'; -import { RoomRepository } from './room.repository.js'; /** * Repository for managing MeetRoomMember entities in MongoDB. * Handles the storage and retrieval of room members. */ @injectable() -export class RoomMemberRepository extends BaseRepository { - private currentRoomRoles: MeetRoomRoles | undefined; - - constructor( - @inject(LoggerService) logger: LoggerService, - @inject(RoomRepository) private roomRepository: RoomRepository - ) { +export class RoomMemberRepository extends BaseRepository< + TRoomMember, + MeetRoomMemberDocument +> { + constructor(@inject(LoggerService) logger: LoggerService) { super(logger, MeetRoomMemberModel); } /** * Transforms a MongoDB document into a domain room member object. - * Computes effective permissions based on base role and custom permissions. * * @param document - The MongoDB document * @returns Room member with computed permissions */ - protected toDomain(document: MeetRoomMemberDocument): MeetRoomMember { - const doc = document.toObject(); - const effectivePermissions = this.computeEffectivePermissions( - this.currentRoomRoles!, - doc.baseRole, - doc.customPermissions - ); - - return { - ...doc, - effectivePermissions - }; + protected toDomain(document: MeetRoomMemberDocument): TRoomMember { + return document.toObject() as TRoomMember; } /** @@ -54,18 +33,9 @@ export class RoomMemberRepository extends BaseRepository): Promise { - const room = await this.roomRepository.findByRoomId(member.roomId); - - if (!room) { - throw errorRoomNotFound(member.roomId); - } - - this.currentRoomRoles = room.roles; - const document = await this.createDocument(member as MeetRoomMember); - const domain = this.toDomain(document); - this.currentRoomRoles = undefined; - return domain; + async create(member: TRoomMember): Promise { + const document = await this.createDocument(member as TRoomMember); + return this.toDomain(document); } /** @@ -75,18 +45,9 @@ export class RoomMemberRepository extends BaseRepository { - const room = await this.roomRepository.findByRoomId(member.roomId); - - if (!room) { - throw errorRoomNotFound(member.roomId); - } - - this.currentRoomRoles = room.roles; + async update(member: TRoomMember): Promise { const document = await this.updateOne({ roomId: member.roomId, memberId: member.memberId }, member); - const domain = this.toDomain(document); - this.currentRoomRoles = undefined; - return domain; + return this.toDomain(document); } /** @@ -96,18 +57,9 @@ export class RoomMemberRepository extends BaseRepository { - const room = await this.roomRepository.findByRoomId(roomId); - - if (!room) { - return null; - } - - this.currentRoomRoles = room.roles; + async findByRoomAndMemberId(roomId: string, memberId: string): Promise { const document = await this.findOne({ roomId, memberId }); - const domain = document ? this.toDomain(document) : null; - this.currentRoomRoles = undefined; - return domain; + return document ? this.toDomain(document) : null; } /** @@ -118,7 +70,7 @@ export class RoomMemberRepository extends BaseRepository { + async findByRoomAndMemberIds(roomId: string, memberIds: string[], fields?: string): Promise { return await this.findAll({ roomId, memberId: { $in: memberIds } }, fields); } @@ -135,46 +87,23 @@ export class RoomMemberRepository extends BaseRepository { - // Get all memberships for this user - const members = await this.findAll({ memberId }, 'roomId,baseRole,customPermissions'); - - if (members.length === 0) { - return []; - } - - // Fetch all rooms - const roomIds = members.map((m) => m.roomId); - const rooms = await this.roomRepository.findByRoomIds(roomIds, 'roomId,roles'); - const roomsMap = new Map(rooms.map((room) => [room.roomId, room])); - - // Filter members where the permission is enabled - const roomIdsWithPermission: string[] = []; - - for (const member of members) { - const room = roomsMap.get(member.roomId); - - if (!room) continue; - - // Compute effective permissions - const basePermissions = room.roles[member.baseRole].permissions; - const effectivePermission = member.customPermissions?.[permission] ?? basePermissions[permission]; - - if (effectivePermission) { - roomIdsWithPermission.push(member.roomId); - } - } - - return roomIdsWithPermission; + const members = await this.findAll( + { + memberId, + [`effectivePermissions.${permission}`]: true + }, + 'roomId' + ); + return members.map((member) => member.roomId); } /** @@ -194,18 +123,10 @@ export class RoomMemberRepository extends BaseRepository { - const room = await this.roomRepository.findByRoomId(roomId); - - if (!room) { - throw errorRoomNotFound(roomId); - } - - this.currentRoomRoles = room.roles; - const { name, fields, @@ -234,8 +155,6 @@ export class RoomMemberRepository extends BaseRepository { await this.deleteMany({ memberId }, false); } - - // ========================================== - // PRIVATE HELPER METHODS - // ========================================== - - /** - * Computes effective permissions by merging base role permissions with custom permissions. - * - * @param roomRoles - The room roles configuration - * @param baseRole - The base role of the member - * @param customPermissions - Optional custom permissions that override the base role - * @returns The effective permissions object - */ - private computeEffectivePermissions( - roomRoles: MeetRoomRoles, - baseRole: MeetRoomMemberRole, - customPermissions?: Partial - ): MeetRoomMemberPermissions { - const basePermissions = roomRoles[baseRole].permissions; - - if (!customPermissions) { - return basePermissions; - } - - return { - ...basePermissions, - ...customPermissions - }; - } } diff --git a/meet-ce/backend/src/services/room-member.service.ts b/meet-ce/backend/src/services/room-member.service.ts index 476739ba..1effea77 100644 --- a/meet-ce/backend/src/services/room-member.service.ts +++ b/meet-ce/backend/src/services/room-member.service.ts @@ -7,12 +7,14 @@ import { MeetRoomMemberRole, MeetRoomMemberTokenMetadata, MeetRoomMemberTokenOptions, + MeetRoomRoles, MeetRoomStatus, MeetUserRole, TrackSource } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { ParticipantInfo } from 'livekit-server-sdk'; +import merge from 'lodash.merge'; import { uid as secureUid } from 'uid/secure'; import { uid } from 'uid/single'; import { MEET_ENV } from '../environment.js'; @@ -105,6 +107,10 @@ export class RoomMemberService { throw new Error('Either userId or name must be provided'); } + // Compute effective permissions + const room = await this.roomService.getMeetRoom(roomId); + const effectivePermissions = this.computeEffectivePermissions(room.roles, baseRole, customPermissions); + const roomMember = { memberId, roomId, @@ -112,11 +118,29 @@ export class RoomMemberService { membershipDate: Date.now(), accessUrl, baseRole, - customPermissions + customPermissions, + effectivePermissions }; return this.roomMemberRepository.create(roomMember); } + /** + * Computes effective permissions by merging base role permissions with custom permissions. + * + * @param roomRoles - The room roles configuration + * @param baseRole - The base role of the member + * @param customPermissions - Optional custom permissions that override the base role + * @returns The effective permissions object + */ + protected computeEffectivePermissions( + roomRoles: MeetRoomRoles, + baseRole: MeetRoomMemberRole, + customPermissions?: Partial + ): MeetRoomMemberPermissions { + const basePermissions = roomRoles[baseRole].permissions; + return merge({}, basePermissions, customPermissions); + } + /** * Checks if a user (registered or external) is a member of a room. * @@ -191,6 +215,14 @@ export class RoomMemberService { member.customPermissions = updates.customPermissions; } + // Recompute effective permissions + const room = await this.roomService.getMeetRoom(roomId); + member.effectivePermissions = this.computeEffectivePermissions( + room.roles, + member.baseRole, + member.customPermissions + ); + const updatedMember = await this.roomMemberRepository.update(member); // If member is currently in a meeting, check if they still have permission to join @@ -219,6 +251,85 @@ export class RoomMemberService { return updatedMember; } + /** + * Updates effective permissions for all members of a room based on the new room roles permissions. + * This method should be called when room roles are updated to ensure all members + * have their effective permissions recalculated. + * + * @param roomId - The ID of the room + * @param roomRoles - The updated room roles configuration + * @returns A promise that resolves when all members have been updated + */ + async updateAllRoomMemberPermissions(roomId: string, roomRoles: MeetRoomRoles): Promise { + this.logger.verbose(`Updating effective permissions for all members in room '${roomId}'`); + + const BATCH_SIZE = 20; // Process members in smaller batches + let nextPageToken: string | undefined; + let totalUpdated = 0; + let batchNumber = 0; + + do { + batchNumber++; + + // Get a batch of members + const { + members, + isTruncated, + nextPageToken: token + } = await this.getAllRoomMembers(roomId, { + maxItems: BATCH_SIZE, + nextPageToken + }); + + if (members.length === 0) { + break; + } + + this.logger.verbose(`Processing batch ${batchNumber} with ${members.length} members in room '${roomId}'`); + + // Update each member's effective permissions in this batch + const updatePromises = members.map(async (member) => { + try { + // Recalculate effective permissions based on new room roles + const effectivePermissions = this.computeEffectivePermissions( + roomRoles, + member.baseRole, + member.customPermissions + ); + + // Update the member with new effective permissions + member.effectivePermissions = effectivePermissions; + await this.roomMemberRepository.update(member); + + this.logger.verbose( + `Updated effective permissions for member '${member.memberId}' in room '${roomId}'` + ); + } catch (error) { + this.logger.error( + `Failed to update effective permissions for member '${member.memberId}' in room '${roomId}':`, + error + ); + // Continue with other members even if one fails + } + }); + + // Wait for all updates in this batch to complete before moving to the next batch + await Promise.all(updatePromises); + + totalUpdated += members.length; + nextPageToken = isTruncated ? token : undefined; + + this.logger.verbose(`Completed batch ${batchNumber}, total updated: ${totalUpdated} members`); + } while (nextPageToken); + + if (totalUpdated === 0) { + this.logger.verbose(`No members found in room '${roomId}' to update`); + return; + } + + this.logger.info(`Successfully updated effective permissions for ${totalUpdated} members in room '${roomId}'`); + } + /** * Updates the currentParticipantIdentity for a room member. * diff --git a/meet-ce/backend/src/services/room.service.ts b/meet-ce/backend/src/services/room.service.ts index bcf638e9..d1abeda9 100644 --- a/meet-ce/backend/src/services/room.service.ts +++ b/meet-ce/backend/src/services/room.service.ts @@ -263,8 +263,12 @@ export class RoomService { // Merge existing roles with new roles (partial update) room.roles = merge({}, room.roles, roles); - await this.roomRepository.update(room); + + // Update existing room members with new effective permissions + const roomMemberService = await this.getRoomMemberService(); + await roomMemberService.updateAllRoomMemberPermissions(roomId, room.roles); + return room; }