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 92779fc6..a5d8abc9 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 @@ -95,10 +95,6 @@ const MeetRoomMemberSchema = new Schema( permissionsUpdatedAt: { type: Number, required: true - }, - currentParticipantIdentity: { - type: String, - required: false } }, { diff --git a/meet-ce/backend/src/services/livekit-webhook.service.ts b/meet-ce/backend/src/services/livekit-webhook.service.ts index 8ac4195a..b0a3777d 100644 --- a/meet-ce/backend/src/services/livekit-webhook.service.ts +++ b/meet-ce/backend/src/services/livekit-webhook.service.ts @@ -158,9 +158,7 @@ export class LivekitWebhookService { } /** - * * Handles the 'participant_joined' event by gathering relevant room and participant information, - * updating the room member's currentParticipantIdentity if applicable, * and sending a room status signal to OpenVidu components. * @param room - Information about the room where the participant joined. * @param participant - Information about the newly joined participant. @@ -169,23 +167,6 @@ export class LivekitWebhookService { // Skip if the participant is not a standard participant if (!this.livekitService.isStandardParticipant(participant)) return; - // Update room member's currentParticipantIdentity if this is an identified member - if (participant.metadata) { - try { - const metadata = JSON.parse(participant.metadata); - - if (metadata.memberId) { - await this.roomMemberService.updateCurrentParticipantIdentity( - room.name, - metadata.memberId, - participant.identity - ); - } - } catch (error) { - this.logger.warn(`Failed to set room member currentParticipantIdentity:`, error); - } - } - try { const { recordings } = await this.recordingService.getAllRecordings({ roomId: room.name }); await this.frontendEventService.sendRoomStatusSignalToOpenViduComponents( @@ -200,7 +181,6 @@ export class LivekitWebhookService { /** * Handles the 'participant_left' event by gathering relevant room and participant information, - * clearing the participant identity from the room member if applicable, * and releasing any reserved participant names. * @param room - Information about the room where the participant left. * @param participant - Information about the participant who left. @@ -209,23 +189,6 @@ export class LivekitWebhookService { // Skip if the participant is not a standard participant if (!this.livekitService.isStandardParticipant(participant)) return; - // Clear room member's currentParticipantIdentity if this is an identified member - if (participant.metadata) { - try { - const metadata = JSON.parse(participant.metadata); - - if (metadata.memberId) { - await this.roomMemberService.updateCurrentParticipantIdentity( - room.name, - metadata.memberId, - undefined - ); - } - } catch (error) { - this.logger.warn(`Failed to clear room member currentParticipantIdentity:`, error); - } - } - try { // Release the participant's reserved name await this.roomMemberService.releaseParticipantName(room.name, participant.name); diff --git a/meet-ce/backend/src/services/room-member.service.ts b/meet-ce/backend/src/services/room-member.service.ts index 80ecc3f7..91a35c90 100644 --- a/meet-ce/backend/src/services/room-member.service.ts +++ b/meet-ce/backend/src/services/room-member.service.ts @@ -29,7 +29,8 @@ import { errorRoomMemberCannotBeOwnerOrAdmin, errorRoomMemberNotFound, errorUnauthorized, - errorUserNotFound + errorUserNotFound, + OpenViduMeetError } from '../models/error.model.js'; import { RoomMemberRepository } from '../repositories/room-member.repository.js'; import { FrontendEventService } from './frontend-event.service.js'; @@ -228,27 +229,11 @@ export class RoomMemberService { const updatedMember = await this.roomMemberRepository.update(member); - // If member is currently in a meeting, check if they still have permission to join - if (updatedMember.currentParticipantIdentity) { - const effectivePermissions = updatedMember.effectivePermissions; - - if (!effectivePermissions.canJoinMeeting) { - // Member lost permission to join meeting, kick them out - try { - await this.kickParticipantFromMeeting(roomId, updatedMember.currentParticipantIdentity); - this.logger.info( - `Kicked participant '${updatedMember.currentParticipantIdentity}' from meeting after losing canJoinMeeting permission (member '${memberId}' in room '${roomId}')` - ); - } catch (error) { - this.logger.warn( - `Failed to kick participant '${updatedMember.currentParticipantIdentity}' from meeting after permission update:`, - error - ); - // Don't throw error, update was already saved - } - } else { - // TODO: Notify participant of role/permission changes if currently in a meeting - } + // If member lost permission to join meeting, kick them out + if (!updatedMember.effectivePermissions.canJoinMeeting) { + await this.kickMembersFromMeetingInBatches(roomId, [memberId]); + } else { + // TODO: Notify participant of role/permission changes if currently in a meeting } return updatedMember; @@ -267,9 +252,10 @@ export class RoomMemberService { this.logger.verbose(`Updating effective permissions for all members in room '${roomId}'`); const BATCH_SIZE = 20; // Process members in smaller batches + let batchNumber = 0; let nextPageToken: string | undefined; let totalUpdated = 0; - let batchNumber = 0; + const totalMembers: MeetRoomMember[] = []; do { batchNumber++; @@ -283,6 +269,7 @@ export class RoomMemberService { maxItems: BATCH_SIZE, nextPageToken }); + totalMembers.push(...members); if (members.length === 0) { break; @@ -331,33 +318,14 @@ export class RoomMemberService { return; } - this.logger.info(`Successfully updated effective permissions for ${totalUpdated} members in room '${roomId}'`); - } + // Kick members who lost canJoinMeeting permission + const membersToKick = totalMembers.filter((m) => !m.effectivePermissions.canJoinMeeting).map((m) => m.memberId); - /** - * Updates the currentParticipantIdentity for a room member. - * - * @param roomId - The ID of the room - * @param memberId - The ID of the member - * @param participantIdentity - The participant identity to set (or undefined to clear it) - */ - async updateCurrentParticipantIdentity( - roomId: string, - memberId: string, - participantIdentity: string | undefined - ): Promise { - const member = await this.getRoomMember(roomId, memberId); - - if (!member) { - this.logger.warn( - `Cannot update currentParticipantIdentity: member '${memberId}' not found in room '${roomId}'` - ); - return; + if (membersToKick.length > 0) { + await this.kickMembersFromMeetingInBatches(roomId, membersToKick); } - member.currentParticipantIdentity = participantIdentity; - await this.roomMemberRepository.update(member); - this.logger.info(`Updated currentParticipantIdentity for member '${memberId}' in room '${roomId}'`); + this.logger.info(`Successfully updated effective permissions for ${totalUpdated} members in room '${roomId}'`); } /** @@ -374,17 +342,7 @@ export class RoomMemberService { } // If member is currently in a meeting, kick them out first - if (member.currentParticipantIdentity) { - try { - await this.kickParticipantFromMeeting(roomId, member.currentParticipantIdentity); - this.logger.info( - `Kicked participant '${member.currentParticipantIdentity}' from meeting before deleting member '${memberId}'` - ); - } catch (error) { - this.logger.warn(`Failed to kick participant from meeting during member deletion:`, error); - // Continue with deletion even if kick fails - } - } + await this.kickMembersFromMeetingInBatches(roomId, [memberId]); return this.roomMemberRepository.deleteByRoomAndMemberId(roomId, memberId); } @@ -415,40 +373,8 @@ export class RoomMemberService { .map((id) => ({ memberId: id, error: 'Room member not found' })); if (foundMemberIds.length > 0) { - // Kick participants that are currently in a meeting - const membersInMeeting = membersToDelete.filter((m) => m.currentParticipantIdentity); - - if (membersInMeeting.length > 0) { - const KICK_BATCH_SIZE = 10; - - // Process kicks in batches to avoid overwhelming the system - for (let i = 0; i < membersInMeeting.length; i += KICK_BATCH_SIZE) { - const batch = membersInMeeting.slice(i, i + KICK_BATCH_SIZE); - - await Promise.allSettled( - batch.map(async (member) => { - try { - await this.kickParticipantFromMeeting(roomId, member.currentParticipantIdentity!); - this.logger.info( - `Kicked participant '${member.currentParticipantIdentity}' from meeting before deleting member '${member.memberId}'` - ); - } catch (error) { - this.logger.warn( - `Failed to kick participant '${member.currentParticipantIdentity}' from meeting during bulk deletion:`, - error - ); - // Continue with deletion even if kick fails - } - }) - ); - - this.logger.verbose(`Processed batch of ${batch.length} participant kicks for room '${roomId}'`); - } - - this.logger.info( - `Kicked ${membersInMeeting.length} participant(s) from meeting before bulk deletion in room '${roomId}'` - ); - } + // Kick participants that are currently in a meeting before deletion + await this.kickMembersFromMeetingInBatches(roomId, foundMemberIds); await this.roomMemberRepository.deleteByRoomIdAndMemberIds(roomId, foundMemberIds); } @@ -473,6 +399,7 @@ export class RoomMemberService { let customPermissions: Partial | undefined = undefined; let effectivePermissions: MeetRoomMemberPermissions; let memberId: string | undefined; + let userId: string | undefined; if (secret) { // Case 1: Secret provided (Anonymous access or External Member) @@ -510,6 +437,8 @@ export class RoomMemberService { throw errorUnauthorized(); } + userId = user.userId; + // Check if user is admin or owner const isOwner = await this.roomService.isRoomOwner(roomId, user.userId); @@ -540,7 +469,8 @@ export class RoomMemberService { participantName, participantIdentity, customPermissions, - memberId + memberId, + userId ); } @@ -558,7 +488,8 @@ export class RoomMemberService { participantName: string, participantIdentity?: string, customPermissions?: Partial, - memberId?: string + memberId?: string, + userId?: string ): Promise { // Check that room is open const room = await this.roomService.getMeetRoom(roomId); @@ -592,9 +523,16 @@ export class RoomMemberService { // Create the Livekit room if it doesn't exist await this.roomService.createLivekitRoom(roomId); - // Create a unique participant identity based on the participant name - const identityPrefix = this.createParticipantIdentityPrefixFromName(participantName) || 'participant'; - participantIdentity = `${identityPrefix}-${uid(15)}`; + if (memberId || userId) { + // Use memberId as participant identity for identified members + // (registered users or external members with a record in the database) + // Use userId as participant identity for registered users without a member record + participantIdentity = memberId || userId; + } else { + // For anonymous users, create a unique participant identity based on the provided participant name + const identityPrefix = this.createParticipantIdentityPrefixFromName(participantName) || 'participant'; + participantIdentity = `${identityPrefix}-${uid(15)}`; + } } else { // REFRESH MODE this.logger.verbose( @@ -764,6 +702,73 @@ export class RoomMemberService { return livekitPermissions; } + /** + * Kicks multiple members from a meeting in batches. + * This method processes the kicks in parallel batches to avoid overwhelming the system. + * + * @param roomId - The ID of the room + * @param memberIds - Array of member IDs to kick from the meeting + * @param batchSize - Number of kicks to process in parallel (default: 10) + */ + protected async kickMembersFromMeetingInBatches( + roomId: string, + memberIds: string[], + batchSize = 10 + ): Promise { + if (memberIds.length === 0) { + return; + } + + let kickedCount = 0; + let failedCount = 0; + + // Process kicks in batches to avoid overwhelming the system + for (let i = 0; i < memberIds.length; i += batchSize) { + const batch = memberIds.slice(i, i + batchSize); + + const results = await Promise.all( + batch.map(async (memberId) => { + try { + await this.kickParticipantFromMeeting(roomId, memberId); + this.logger.verbose(`Kicked participant '${memberId}' from meeting in room '${roomId}'`); + return true; + } catch (error) { + const isParticipantNotFound = error instanceof OpenViduMeetError && error.statusCode === 404; + + if (!isParticipantNotFound) { + // Real error, log warning + this.logger.warn( + `Failed to kick participant '${memberId}' from meeting in room '${roomId}':`, + error + ); + return false; + } + + // Participant not in meeting, nothing to do + return true; + } + }) + ); + + // Count only successful kicks and real failures (not "participant not found") + results.forEach((result) => { + if (result) { + kickedCount++; + } else { + failedCount++; + } + }); + } + + if (kickedCount > 0) { + this.logger.info(`Kicked ${kickedCount} participant(s) from meeting in room '${roomId}'`); + } + + if (failedCount > 0) { + this.logger.warn(`Failed to kick ${failedCount} participant(s) from meeting in room '${roomId}'`); + } + } + async kickParticipantFromMeeting(roomId: string, participantIdentity: string): Promise { this.logger.verbose(`Kicking participant '${participantIdentity}' from room '${roomId}'`); return this.livekitService.deleteParticipant(roomId, participantIdentity); diff --git a/meet-ce/backend/tests/helpers/assertion-helpers.ts b/meet-ce/backend/tests/helpers/assertion-helpers.ts index 19ede058..68cdb97d 100644 --- a/meet-ce/backend/tests/helpers/assertion-helpers.ts +++ b/meet-ce/backend/tests/helpers/assertion-helpers.ts @@ -663,7 +663,9 @@ export const expectValidRoomMemberTokenResponse = ( expect(decodedToken).toHaveProperty('name', participantName); expect(decodedToken).toHaveProperty('sub'); - if (participantIdentityPrefix) { + if (memberId) { + expect(decodedToken.sub).toBe(memberId); + } else if (participantIdentityPrefix) { expect(decodedToken.sub?.startsWith(participantIdentityPrefix)).toBe(true); } diff --git a/meet-ce/backend/tests/integration/api/room-members/bulk-delete-room-members.test.ts b/meet-ce/backend/tests/integration/api/room-members/bulk-delete-room-members.test.ts index c8c6de96..059c96d3 100644 --- a/meet-ce/backend/tests/integration/api/room-members/bulk-delete-room-members.test.ts +++ b/meet-ce/backend/tests/integration/api/room-members/bulk-delete-room-members.test.ts @@ -1,11 +1,8 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import { MeetRoomMember, MeetRoomMemberRole, MeetUserRole } from '@openvidu-meet/typings'; import { container } from '../../../../src/config/dependency-injector.config.js'; -import { MEET_ENV } from '../../../../src/environment.js'; import { OpenViduMeetError } from '../../../../src/models/error.model.js'; -import { RoomMemberRepository } from '../../../../src/repositories/room-member.repository.js'; import { LiveKitService } from '../../../../src/services/livekit.service.js'; -import { TokenService } from '../../../../src/services/token.service.js'; import { expectValidationError } from '../../../helpers/assertion-helpers.js'; import { bulkDeleteRoomMembers, @@ -15,12 +12,10 @@ import { deleteAllRooms, deleteAllUsers, disconnectFakeParticipants, - generateRoomMemberTokenRequest, getRoomMember, getUser, joinFakeParticipant, - startTestServer, - updateParticipantMetadata + startTestServer } from '../../../helpers/request-helpers.js'; describe('Bulk Delete Room Members API Tests', () => { @@ -190,52 +185,13 @@ describe('Bulk Delete Room Members API Tests', () => { const member1 = member1Response.body as MeetRoomMember; const member2 = member2Response.body as MeetRoomMember; - // Generate tokens and join meeting for both - const token1Response = await generateRoomMemberTokenRequest(roomId, { - secret: member1.memberId, - joinMeeting: true, - participantName: 'Meeting Member 1' - }); - const token2Response = await generateRoomMemberTokenRequest(roomId, { - secret: member2.memberId, - joinMeeting: true, - participantName: 'Meeting Member 2' - }); + // Participant identity is the same as memberId for members + const participantIdentity1 = member1.memberId; + const participantIdentity2 = member2.memberId; - // Get participant identities from tokens - const tokenService = container.get(TokenService); - const decodedToken1 = await tokenService.verifyToken(token1Response.body.token); - const decodedToken2 = await tokenService.verifyToken(token2Response.body.token); - const participantIdentity1 = decodedToken1.sub!; - const participantIdentity2 = decodedToken2.sub!; - - // Join fake participants and update metadata + // Join fake participants to the room to simulate real join await joinFakeParticipant(roomId, participantIdentity1); - await updateParticipantMetadata(roomId, participantIdentity1, { - iat: Date.now(), - livekitUrl: MEET_ENV.LIVEKIT_URL, - roomId, - memberId: member1.memberId, - baseRole: MeetRoomMemberRole.SPEAKER, - effectivePermissions: member1.effectivePermissions - }); - await joinFakeParticipant(roomId, participantIdentity2); - await updateParticipantMetadata(roomId, participantIdentity2, { - iat: Date.now(), - livekitUrl: MEET_ENV.LIVEKIT_URL, - roomId, - memberId: member2.memberId, - baseRole: MeetRoomMemberRole.MODERATOR, - effectivePermissions: member2.effectivePermissions - }); - - // Update room members currentParticipantIdentity - const roomMemberRepository = container.get(RoomMemberRepository); - member1.currentParticipantIdentity = participantIdentity1; - member2.currentParticipantIdentity = participantIdentity2; - await roomMemberRepository.update(member1); - await roomMemberRepository.update(member2); // Verify both participants exist const livekitService = container.get(LiveKitService); diff --git a/meet-ce/backend/tests/integration/api/room-members/delete-room-member.test.ts b/meet-ce/backend/tests/integration/api/room-members/delete-room-member.test.ts index f24a5166..5492b487 100644 --- a/meet-ce/backend/tests/integration/api/room-members/delete-room-member.test.ts +++ b/meet-ce/backend/tests/integration/api/room-members/delete-room-member.test.ts @@ -3,9 +3,7 @@ import { MeetRoomMember, MeetRoomMemberRole, MeetUserRole } from '@openvidu-meet import { container } from '../../../../src/config/dependency-injector.config.js'; import { MEET_ENV } from '../../../../src/environment.js'; import { OpenViduMeetError } from '../../../../src/models/error.model.js'; -import { RoomMemberRepository } from '../../../../src/repositories/room-member.repository.js'; import { LiveKitService } from '../../../../src/services/livekit.service.js'; -import { TokenService } from '../../../../src/services/token.service.js'; import { createRoom, createRoomMember, @@ -14,7 +12,6 @@ import { deleteAllUsers, deleteRoomMember, disconnectFakeParticipants, - generateRoomMemberTokenRequest, getRoomMember, getUser, joinFakeParticipant, @@ -110,20 +107,8 @@ describe('Room Members API Tests', () => { const member = createResponse.body as MeetRoomMember; const memberId = member.memberId; - // Generate room member token for joining meeting - const tokenResponse = await generateRoomMemberTokenRequest(roomId, { - secret: memberId, - joinMeeting: true, - participantName: 'Test Member' - }); - const roomMemberToken = tokenResponse.body.token; - - // Get participant identity from token - const tokenService = container.get(TokenService); - const decodedToken = await tokenService.verifyToken(roomMemberToken); - const participantIdentity = decodedToken.sub!; - - // Join fake participant to the room and update metadata to simulate real join + // Join fake participant to the room to simulate real join + const participantIdentity = memberId; // Participant identity is the same as memberId for members await joinFakeParticipant(roomId, participantIdentity); await updateParticipantMetadata(roomId, participantIdentity, { iat: Date.now(), @@ -134,11 +119,6 @@ describe('Room Members API Tests', () => { effectivePermissions: member.effectivePermissions }); - // Update room member currentParticipantIdentity manually to simulate real join - const roomMemberRepository = container.get(RoomMemberRepository); - member.currentParticipantIdentity = participantIdentity; - await roomMemberRepository.update(member); - // Verify participant exists before deletion const livekitService = container.get(LiveKitService); const participant = await livekitService.getParticipant(roomId, participantIdentity); diff --git a/meet-ce/backend/tests/integration/api/room-members/update-room-member.test.ts b/meet-ce/backend/tests/integration/api/room-members/update-room-member.test.ts index 563b28b7..28888be3 100644 --- a/meet-ce/backend/tests/integration/api/room-members/update-room-member.test.ts +++ b/meet-ce/backend/tests/integration/api/room-members/update-room-member.test.ts @@ -1,11 +1,8 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import { MeetRoomMember, MeetRoomMemberRole, MeetRoomRoles, MeetUserRole } from '@openvidu-meet/typings'; import { container } from '../../../../src/config/dependency-injector.config.js'; -import { MEET_ENV } from '../../../../src/environment.js'; import { OpenViduMeetError } from '../../../../src/models/error.model.js'; -import { RoomMemberRepository } from '../../../../src/repositories/room-member.repository.js'; import { LiveKitService } from '../../../../src/services/livekit.service.js'; -import { TokenService } from '../../../../src/services/token.service.js'; import { expectValidationError } from '../../../helpers/assertion-helpers.js'; import { createRoom, @@ -14,12 +11,10 @@ import { deleteAllRooms, deleteAllUsers, disconnectFakeParticipants, - generateRoomMemberTokenRequest, getRoomMember, joinFakeParticipant, sleep, startTestServer, - updateParticipantMetadata, updateRoomMember } from '../../../helpers/request-helpers.js'; @@ -260,34 +255,9 @@ describe('Room Members API Tests', () => { const member = createResponse.body as MeetRoomMember; const memberId = member.memberId; - // Generate room member token for joining meeting - const tokenResponse = await generateRoomMemberTokenRequest(roomId, { - secret: memberId, - joinMeeting: true, - participantName: 'Test Member' - }); - const roomMemberToken = tokenResponse.body.token; - - // Get participant identity from token - const tokenService = container.get(TokenService); - const decodedToken = await tokenService.verifyToken(roomMemberToken); - const participantIdentity = decodedToken.sub!; - - // Join fake participant to the room and update metadata to simulate real join + // Join fake participant to the room to simulate real join + const participantIdentity = memberId; // Participant identity is the same as memberId for members await joinFakeParticipant(roomId, participantIdentity); - await updateParticipantMetadata(roomId, participantIdentity, { - iat: Date.now(), - livekitUrl: MEET_ENV.LIVEKIT_URL, - roomId, - memberId, - baseRole: MeetRoomMemberRole.SPEAKER, - effectivePermissions: roomRoles.speaker.permissions - }); - - // Update room member currentParticipantIdentity manually to simulate real join - const roomMemberRepository = container.get(RoomMemberRepository); - member.currentParticipantIdentity = participantIdentity; - await roomMemberRepository.update(member); // Verify participant exists before deletion const livekitService = container.get(LiveKitService); diff --git a/meet-ce/typings/src/room-member.ts b/meet-ce/typings/src/room-member.ts index 67a92b8a..6e39bc6a 100644 --- a/meet-ce/typings/src/room-member.ts +++ b/meet-ce/typings/src/room-member.ts @@ -25,7 +25,6 @@ export interface MeetRoomMember { customPermissions?: Partial; // Custom permissions for the member (if any) effectivePermissions: MeetRoomMemberPermissions; // Effective permissions for the member (base role + custom permissions) permissionsUpdatedAt: number; // Timestamp when the effective permissions were last updated - currentParticipantIdentity?: string; // The participant identity if the member is currently in a meeting, undefined otherwise } /**