diff --git a/backend/src/controllers/participant.controller.ts b/backend/src/controllers/participant.controller.ts index ed917d2..2daa2cc 100644 --- a/backend/src/controllers/participant.controller.ts +++ b/backend/src/controllers/participant.controller.ts @@ -31,7 +31,7 @@ export const generateParticipantToken = async (req: Request, res: Response) => { try { const claims = tokenService.getClaimsIgnoringExpiration(previousToken); - const metadata = JSON.parse(claims.metadata || '{}'); + const metadata = participantService.parseMetadata(claims.metadata || '{}'); currentRoles = metadata.roles; } catch (error) { logger.verbose('Error extracting roles from previous token:', error); @@ -80,11 +80,12 @@ export const refreshParticipantToken = async (req: Request, res: Response) => { } // Extract roles from the previous token + const participantService = container.get(ParticipantService); let currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[] = []; try { const claims = tokenService.getClaimsIgnoringExpiration(previousToken); - const metadata = JSON.parse(claims.metadata || '{}'); + const metadata = participantService.parseMetadata(claims.metadata || '{}'); currentRoles = metadata.roles; } catch (err) { logger.verbose('Error extracting roles from previous token:', err); @@ -94,7 +95,6 @@ export const refreshParticipantToken = async (req: Request, res: Response) => { const participantOptions: ParticipantOptions = req.body; const { roomId } = participantOptions; - const participantService = container.get(ParticipantService); try { logger.verbose(`Refreshing participant token for room '${roomId}'`); @@ -111,11 +111,26 @@ export const refreshParticipantToken = async (req: Request, res: Response) => { } }; +export const updateParticipant = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const participantService = container.get(ParticipantService); + const { roomId, participantIdentity } = req.params; + const { role } = req.body; + + try { + logger.verbose(`Changing role of participant '${participantIdentity}' in room '${roomId}' to '${role}'`); + await participantService.updateParticipantRole(roomId, participantIdentity, role); + res.status(200).json({ message: `Participant '${participantIdentity}' role updated to '${role}'` }); + } catch (error) { + handleError(res, error, `changing role for participant '${participantIdentity}' in room '${roomId}'`); + } +}; + export const deleteParticipant = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const roomService = container.get(RoomService); const participantService = container.get(ParticipantService); - const { roomId, participantName } = req.params; + const { roomId, participantIdentity } = req.params; // Check if the room exists try { @@ -125,25 +140,10 @@ export const deleteParticipant = async (req: Request, res: Response) => { } try { - logger.verbose(`Deleting participant '${participantName}' from room '${roomId}'`); - await participantService.deleteParticipant(roomId, participantName); + logger.verbose(`Deleting participant '${participantIdentity}' from room '${roomId}'`); + await participantService.deleteParticipant(roomId, participantIdentity); res.status(200).json({ message: 'Participant deleted' }); } catch (error) { - handleError(res, error, `deleting participant '${participantName}' from room '${roomId}'`); - } -}; - -export const updateParticipant = async (req: Request, res: Response) => { - const logger = container.get(LoggerService); - const participantService = container.get(ParticipantService); - const { roomId, participantName } = req.params; - const { role } = req.body; - - try { - logger.verbose(`Changing role of participant '${participantName}' in room '${roomId}' to '${role}'`); - await participantService.updateParticipantRole(roomId, participantName, role); - res.status(200).json({ message: `Participant '${participantName}' role updated to ${role}` }); - } catch (error) { - handleError(res, error, `changing role for participant '${participantName}' in room '${roomId}'`); + handleError(res, error, `deleting participant '${participantIdentity}' from room '${roomId}'`); } }; diff --git a/backend/src/routes/meeting.routes.ts b/backend/src/routes/meeting.routes.ts index d4b894b..f2f9b7b 100644 --- a/backend/src/routes/meeting.routes.ts +++ b/backend/src/routes/meeting.routes.ts @@ -2,7 +2,13 @@ import bodyParser from 'body-parser'; import { Router } from 'express'; import * as meetingCtrl from '../controllers/meeting.controller.js'; import * as participantCtrl from '../controllers/participant.controller.js'; -import { participantTokenValidator, withAuth, withModeratorPermissions, withValidParticipantRole, withValidRoomId } from '../middlewares/index.js'; +import { + participantTokenValidator, + validateUpdateParticipantRequest, + withAuth, + withModeratorPermissions, + withValidRoomId +} from '../middlewares/index.js'; export const internalMeetingRouter = Router(); internalMeetingRouter.use(bodyParser.urlencoded({ extended: true })); @@ -12,23 +18,22 @@ internalMeetingRouter.use(bodyParser.json()); internalMeetingRouter.delete( '/:roomId', withAuth(participantTokenValidator), - withModeratorPermissions, withValidRoomId, + withModeratorPermissions, meetingCtrl.endMeeting ); -internalMeetingRouter.delete( - '/:roomId/participants/:participantName', - withAuth(participantTokenValidator), - withModeratorPermissions, - withValidRoomId, - participantCtrl.deleteParticipant -); - internalMeetingRouter.patch( - '/:roomId/participants/:participantName', + '/:roomId/participants/:participantIdentity', withAuth(participantTokenValidator), - withModeratorPermissions, withValidRoomId, - withValidParticipantRole, + withModeratorPermissions, + validateUpdateParticipantRequest, participantCtrl.updateParticipant ); +internalMeetingRouter.delete( + '/:roomId/participants/:participantIdentity', + withAuth(participantTokenValidator), + withValidRoomId, + withModeratorPermissions, + participantCtrl.deleteParticipant +); diff --git a/backend/src/services/livekit.service.ts b/backend/src/services/livekit.service.ts index e9859c5..5d33427 100644 --- a/backend/src/services/livekit.service.ts +++ b/backend/src/services/livekit.service.ts @@ -23,8 +23,8 @@ import { internalError, OpenViduMeetError } from '../models/error.model.js'; -import { LoggerService } from './index.js'; import { chunkArray } from '../utils/array.utils.js'; +import { LoggerService } from './index.js'; @injectable() export class LiveKitService { @@ -77,7 +77,7 @@ export class LiveKitService { */ async roomHasParticipants(roomName: string): Promise { try { - const participants = await this.roomClient.listParticipants(roomName); + const participants = await this.listRoomParticipants(roomName); return participants.length > 0; } catch (error) { return false; @@ -167,20 +167,35 @@ export class LiveKitService { } } + /** + * Lists all participants in a LiveKit room. + * + * @param roomName - The name of the room to list participants from + * @returns A promise that resolves to an array of participant information + */ + async listRoomParticipants(roomName: string): Promise { + try { + return await this.roomClient.listParticipants(roomName); + } catch (error) { + this.logger.error(`Error listing participants for room '${roomName}': ${error}`); + throw internalError(`listing participants for room '${roomName}'`); + } + } + /** * Retrieves information about a specific participant in a LiveKit room. * * @param roomName - The name of the room where the participant is located - * @param participantName - The name of the participant to retrieve + * @param participantIdentity - The identity of the participant to retrieve * @returns A Promise that resolves to the participant's information * @throws An internal error if the participant cannot be found or another error occurs */ - async getParticipant(roomName: string, participantName: string): Promise { + async getParticipant(roomName: string, participantIdentity: string): Promise { try { - return await this.roomClient.getParticipant(roomName, participantName); + return await this.roomClient.getParticipant(roomName, participantIdentity); } catch (error) { - this.logger.warn(`Participant ${participantName} not found in room ${roomName}: ${error}`); - throw errorParticipantNotFound(participantName, roomName); + this.logger.warn(`Participant ${participantIdentity} not found in room ${roomName}: ${error}`); + throw errorParticipantNotFound(participantIdentity, roomName); } } @@ -188,31 +203,31 @@ export class LiveKitService { * Updates the metadata of a participant in a LiveKit room. * * @param roomName - The name of the room where the participant is located - * @param participantName - The name of the participant whose metadata will be updated + * @param participantIdentity - The identity of the participant whose metadata will be updated * @param metadata - The new metadata to set for the participant * @returns A Promise that resolves when the metadata has been successfully updated * @throws An internal error if there is an issue updating the metadata */ - async updateParticipantMetadata(roomName: string, participantName: string, metadata: string): Promise { + async updateParticipantMetadata(roomName: string, participantIdentity: string, metadata: string): Promise { try { - await this.roomClient.updateParticipant(roomName, participantName, metadata); - this.logger.verbose(`Updated metadata for participant ${participantName} in room ${roomName}`); + await this.roomClient.updateParticipant(roomName, participantIdentity, metadata); + this.logger.verbose(`Updated metadata for participant '${participantIdentity}' in room '${roomName}'`); } catch (error) { this.logger.error( - `Error updating metadata for participant ${participantName} in room ${roomName}: ${error}` + `Error updating metadata for participant '${participantIdentity}' in room '${roomName}': ${error}` ); - throw internalError(`updating metadata for participant '${participantName}' in room '${roomName}'`); + throw internalError(`updating metadata for participant '${participantIdentity}' in room '${roomName}'`); } } - async deleteParticipant(participantName: string, roomName: string): Promise { - const participantExists = await this.participantExists(roomName, participantName); + async deleteParticipant(roomName: string, participantIdentity: string): Promise { + const participantExists = await this.participantExists(roomName, participantIdentity); if (!participantExists) { - throw errorParticipantNotFound(participantName, roomName); + throw errorParticipantNotFound(participantIdentity, roomName); } - await this.roomClient.removeParticipant(roomName, participantName); + await this.roomClient.removeParticipant(roomName, participantIdentity); } async sendData(roomName: string, rawData: Record, options: SendDataOptions): Promise { @@ -369,10 +384,10 @@ export class LiveKitService { return participant.identity.startsWith('EG_') && participant.permission?.recorder === true; } - private async participantExists(roomName: string, participantName: string): Promise { + private async participantExists(roomName: string, participantIdentity: string): Promise { try { - const participants: ParticipantInfo[] = await this.roomClient.listParticipants(roomName); - return participants.some((participant) => participant.identity === participantName); + const participants: ParticipantInfo[] = await this.listRoomParticipants(roomName); + return participants.some((participant) => participant.identity === participantIdentity); } catch (error: any) { this.logger.error(error); diff --git a/backend/src/services/participant.service.ts b/backend/src/services/participant.service.ts index 13508bf..6a6df01 100644 --- a/backend/src/services/participant.service.ts +++ b/backend/src/services/participant.service.ts @@ -7,9 +7,10 @@ import { } from '@typings-ce'; import { inject, injectable } from 'inversify'; import { ParticipantInfo } from 'livekit-server-sdk'; +import { MeetRoomHelper } from '../helpers/room.helper.js'; +import { validateMeetTokenMetadata } from '../middlewares/index.js'; import { errorParticipantAlreadyExists, errorParticipantNotFound } from '../models/error.model.js'; import { FrontendEventService, LiveKitService, LoggerService, RoomService, TokenService } from './index.js'; -import { MeetRoomHelper } from '../helpers/room.helper.js'; @injectable() export class ParticipantService { @@ -64,26 +65,26 @@ export class ParticipantService { return this.tokenService.generateParticipantToken(participantOptions, permissions.livekit, currentRoles, role); } - async getParticipant(roomId: string, participantName: string): Promise { - this.logger.verbose(`Fetching participant '${participantName}'`); - return this.livekitService.getParticipant(roomId, participantName); + async getParticipant(roomId: string, participantIdentity: string): Promise { + this.logger.verbose(`Fetching participant '${participantIdentity}'`); + return this.livekitService.getParticipant(roomId, participantIdentity); } - async participantExists(roomId: string, participantName: string): Promise { - this.logger.verbose(`Checking if participant '${participantName}' exists in room '${roomId}'`); + async participantExists(roomId: string, participantIdentity: string): Promise { + this.logger.verbose(`Checking if participant '${participantIdentity}' exists in room '${roomId}'`); try { - const participant = await this.getParticipant(roomId, participantName); - return participant !== null; + await this.getParticipant(roomId, participantIdentity); + return true; } catch (error) { return false; } } - async deleteParticipant(roomId: string, participantName: string): Promise { - this.logger.verbose(`Deleting participant '${participantName}' from room '${roomId}'`); + async deleteParticipant(roomId: string, participantIdentity: string): Promise { + this.logger.verbose(`Deleting participant '${participantIdentity}' from room '${roomId}'`); - return this.livekitService.deleteParticipant(participantName, roomId); + return this.livekitService.deleteParticipant(roomId, participantIdentity); } getParticipantPermissions(roomId: string, role: ParticipantRole, addJoinPermission = true): ParticipantPermissions { @@ -97,36 +98,42 @@ export class ParticipantService { } } - async updateParticipantRole(roomId: string, participantName: string, newRole: ParticipantRole): Promise { + async updateParticipantRole(roomId: string, participantIdentity: string, newRole: ParticipantRole): Promise { try { const meetRoom = await this.roomService.getMeetRoom(roomId); - const participant = await this.getParticipant(roomId, participantName); + const participant = await this.getParticipant(roomId, participantIdentity); + const metadata: MeetTokenMetadata = this.parseMetadata(participant.metadata); - const metadata: MeetTokenMetadata = this.parseMetadata(participant!.metadata); + // Update selected role and roles array + metadata.selectedRole = newRole; + const currentRoles = metadata.roles; - if (!metadata || typeof metadata !== 'object') { - throw new Error(`Invalid metadata for participant ${participantName}`); + if (!currentRoles.some((r) => r.role === newRole)) { + const { openvidu } = this.getParticipantPermissions(roomId, newRole); + currentRoles.push({ role: newRole, permissions: openvidu }); } - // TODO: Should we update the roles array as well? - metadata.selectedRole = newRole; - - await this.livekitService.updateParticipantMetadata(roomId, participantName, JSON.stringify(metadata)); + await this.livekitService.updateParticipantMetadata(roomId, participantIdentity, JSON.stringify(metadata)); const { speakerSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(meetRoom); - const secret = newRole === ParticipantRole.MODERATOR ? moderatorSecret : speakerSecret; - await this.frontendEventService.sendParticipantRoleUpdatedSignal(roomId, participantName, newRole, secret); + await this.frontendEventService.sendParticipantRoleUpdatedSignal( + roomId, + participantIdentity, + newRole, + secret + ); } catch (error) { - this.logger.error('Error changing participant role:', error); + this.logger.error('Error updating participant role:', error); throw error; } } - protected parseMetadata(metadata: string): MeetTokenMetadata { + parseMetadata(metadata: string): MeetTokenMetadata { try { - return JSON.parse(metadata); + const parsedMetadata = JSON.parse(metadata); + return validateMeetTokenMetadata(parsedMetadata); } catch (error) { this.logger.error('Failed to parse participant metadata:', error); throw new Error('Invalid participant metadata format'); diff --git a/backend/tests/integration/api/meetings/delete-participant.test.ts b/backend/tests/integration/api/meetings/delete-participant.test.ts index c0ef134..9b2b389 100644 --- a/backend/tests/integration/api/meetings/delete-participant.test.ts +++ b/backend/tests/integration/api/meetings/delete-participant.test.ts @@ -11,7 +11,7 @@ import { } from '../../../helpers/request-helpers.js'; import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; -const participantName = 'TEST_PARTICIPANT'; +const participantIdentity = 'TEST_PARTICIPANT'; describe('Meetings API Tests', () => { let livekitService: LiveKitService; @@ -34,17 +34,17 @@ describe('Meetings API Tests', () => { it('should remove participant from LiveKit room', async () => { // Check if participant exists before deletion - const participant = await livekitService.getParticipant(roomData.room.roomId, participantName); + const participant = await livekitService.getParticipant(roomData.room.roomId, participantIdentity); expect(participant).toBeDefined(); - expect(participant.identity).toBe(participantName); + expect(participant.identity).toBe(participantIdentity); // Delete the participant - const response = await deleteParticipant(roomData.room.roomId, participantName, roomData.moderatorCookie); + const response = await deleteParticipant(roomData.room.roomId, participantIdentity, roomData.moderatorCookie); expect(response.status).toBe(200); // Check if the participant has been removed from LiveKit try { - await livekitService.getParticipant(roomData.room.roomId, participantName); + await livekitService.getParticipant(roomData.room.roomId, participantIdentity); } catch (error) { expect((error as OpenViduMeetError).statusCode).toBe(404); } @@ -65,7 +65,7 @@ describe('Meetings API Tests', () => { let response = await deleteRoom(roomData.room.roomId, { force: true }); expect(response.status).toBe(204); - response = await deleteParticipant(roomData.room.roomId, participantName, roomData.moderatorCookie); + response = await deleteParticipant(roomData.room.roomId, participantIdentity, roomData.moderatorCookie); expect(response.status).toBe(404); expect(response.body.error).toBe('Room Error'); });