diff --git a/meet-ce/backend/src/controllers/room-member.controller.ts b/meet-ce/backend/src/controllers/room-member.controller.ts index 558afaca..bc0b2c9d 100644 --- a/meet-ce/backend/src/controllers/room-member.controller.ts +++ b/meet-ce/backend/src/controllers/room-member.controller.ts @@ -1,9 +1,124 @@ -import { MeetRoomMemberTokenOptions } from '@openvidu-meet/typings'; +import { MeetRoomMemberFilters, MeetRoomMemberTokenOptions } from '@openvidu-meet/typings'; import { Request, Response } from 'express'; import { container } from '../config/dependency-injector.config.js'; -import { handleError } from '../models/error.model.js'; +import { INTERNAL_CONFIG } from '../config/internal-config.js'; +import { errorRoomMemberNotFound, handleError } from '../models/error.model.js'; import { LoggerService } from '../services/logger.service.js'; import { RoomMemberService } from '../services/room-member.service.js'; +import { getBaseUrl } from '../utils/url.utils.js'; + +export const createRoomMember = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const roomMemberService = container.get(RoomMemberService); + + const { roomId } = req.params; + const memberOptions = req.body; + + try { + logger.verbose(`Adding member in room '${roomId}'`); + const member = await roomMemberService.createRoomMember(roomId, memberOptions); + res.set( + 'Location', + `${getBaseUrl()}${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}/members/${member.memberId}` + ); + return res.status(201).json(member); + } catch (error) { + handleError(res, error, `adding member in room '${roomId}'`); + } +}; + +export const getRoomMembers = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const roomMemberService = container.get(RoomMemberService); + + const { roomId } = req.params; + const filters = req.query as MeetRoomMemberFilters; + + try { + logger.verbose(`Getting members for room '${roomId}'`); + const { members, isTruncated, nextPageToken } = await roomMemberService.getAllRoomMembers(roomId, filters); + const maxItems = Number(filters.maxItems); + return res.status(200).json({ members, pagination: { isTruncated, nextPageToken, maxItems } }); + } catch (error) { + handleError(res, error, `getting members for room '${roomId}'`); + } +}; + +export const getRoomMember = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const roomMemberService = container.get(RoomMemberService); + + const { roomId, memberId } = req.params; + + try { + logger.verbose(`Getting member '${memberId}' from room '${roomId}'`); + const member = await roomMemberService.getRoomMember(roomId, memberId); + + if (!member) { + throw errorRoomMemberNotFound(roomId, memberId); + } + + return res.status(200).json(member); + } catch (error) { + handleError(res, error, `getting member '${memberId}' from room '${roomId}'`); + } +}; + +export const updateRoomMember = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const roomMemberService = container.get(RoomMemberService); + + const { roomId, memberId } = req.params; + const updates = req.body; + + try { + logger.verbose(`Updating member '${memberId}' in room '${roomId}'`); + const member = await roomMemberService.updateRoomMember(roomId, memberId, updates); + return res.status(200).json(member); + } catch (error) { + handleError(res, error, `updating member '${memberId}' in room '${roomId}'`); + } +}; + +export const deleteRoomMember = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const roomMemberService = container.get(RoomMemberService); + + const { roomId, memberId } = req.params; + + try { + logger.verbose(`Deleting member '${memberId}' from room '${roomId}'`); + await roomMemberService.deleteRoomMember(roomId, memberId); + return res.status(200).json({ message: `Member '${memberId}' deleted successfully from room '${roomId}'` }); + } catch (error) { + handleError(res, error, `deleting member '${memberId}' from room '${roomId}'`); + } +}; + +export const bulkDeleteRoomMembers = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + const roomMemberService = container.get(RoomMemberService); + + const { roomId } = req.params; + const { memberIds } = req.body; + + try { + logger.verbose(`Deleting members from room '${roomId}' with IDs: ${memberIds.join(', ')}`); + const { deleted, failed } = await roomMemberService.bulkDeleteRoomMembers(roomId, memberIds); + + // All room members were successfully deleted + if (deleted.length > 0 && failed.length === 0) { + return res.status(200).json({ message: 'All room members deleted successfully', deleted }); + } + + // Some or all room members could not be deleted + return res + .status(400) + .json({ message: `${failed.length} room member(s) could not be deleted`, deleted, failed }); + } catch (error) { + handleError(res, error, `bulk deleting members from room '${roomId}'`); + } +}; export const generateRoomMemberToken = async (req: Request, res: Response) => { const logger = container.get(LoggerService); diff --git a/meet-ce/backend/src/models/error.model.ts b/meet-ce/backend/src/models/error.model.ts index 1d2826b1..6b94d1c0 100644 --- a/meet-ce/backend/src/models/error.model.ts +++ b/meet-ce/backend/src/models/error.model.ts @@ -239,6 +239,10 @@ export const errorInvalidRoomMemberRole = (): OpenViduMeetError => { return new OpenViduMeetError('Room Error', 'No valid room member role provided', 400); }; +export const errorRoomMemberNotFound = (roomId: string, memberId: string): OpenViduMeetError => { + return new OpenViduMeetError('Room Member Error', `Room member '${memberId}' not found in room '${roomId}'`, 404); +}; + // Participant errors export const errorParticipantNotFound = (participantIdentity: string, roomId: string): OpenViduMeetError => { diff --git a/meet-ce/backend/src/repositories/room-member.repository.ts b/meet-ce/backend/src/repositories/room-member.repository.ts index 8fd8af05..1a51776d 100644 --- a/meet-ce/backend/src/repositories/room-member.repository.ts +++ b/meet-ce/backend/src/repositories/room-member.repository.ts @@ -1,10 +1,10 @@ import { MeetRoomMember, MeetRoomMemberPermissions, MeetRoomMemberRole, MeetRoomRoles } 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'; -import { errorRoomNotFound } from '../models/error.model.js'; /** * Repository for managing MeetRoomMember entities in MongoDB. @@ -104,6 +104,16 @@ export class RoomMemberRepository extends BaseRepository { + return await this.findAll({ memberId: { $in: memberIds } }); + } + /** * Finds members of a room with optional filtering, pagination, and sorting. * @@ -186,15 +196,15 @@ export class RoomMemberRepository extends BaseRepository { - await this.deleteMany({ roomId }); - } + /** + * Removes all members from a room. + * + * @param roomId - The ID of the room + * @throws Error if members could not be deleted + */ + async deleteAllByRoomId(roomId: string): Promise { + await this.deleteMany({ roomId }); + } // ========================================== // PRIVATE HELPER METHODS diff --git a/meet-ce/backend/src/services/room-member.service.ts b/meet-ce/backend/src/services/room-member.service.ts index 6fcee613..5e04a092 100644 --- a/meet-ce/backend/src/services/room-member.service.ts +++ b/meet-ce/backend/src/services/room-member.service.ts @@ -1,23 +1,42 @@ import { - MeetRecordingAccess, + LiveKitPermissions, + MeetRoomMember, + MeetRoomMemberFilters, + MeetRoomMemberOptions, MeetRoomMemberPermissions, MeetRoomMemberRole, MeetRoomMemberTokenMetadata, MeetRoomMemberTokenOptions, - MeetRoomStatus + MeetRoomStatus, + MeetUserRole, + TrackSource } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { ParticipantInfo } from 'livekit-server-sdk'; +import { uid as secureUid } from 'uid/secure'; import { uid } from 'uid/single'; +import { MEET_ENV } from '../environment.js'; import { MeetRoomHelper } from '../helpers/room.helper.js'; -import { validateRoomMemberTokenMetadata } from '../middlewares/request-validators/room-validator.middleware.js'; -import { errorInvalidRoomSecret, errorParticipantNotFound, errorRoomClosed } from '../models/error.model.js'; +import { UtilsHelper } from '../helpers/utils.helper.js'; +import { validateRoomMemberTokenMetadata } from '../middlewares/request-validators/room-member-validator.middleware.js'; +import { + errorInsufficientPermissions, + errorInvalidRoomSecret, + errorParticipantNotFound, + errorRoomClosed, + errorRoomMemberNotFound, + errorUnauthorized, + errorUserNotFound +} from '../models/error.model.js'; +import { RoomMemberRepository } from '../repositories/room-member.repository.js'; import { FrontendEventService } from './frontend-event.service.js'; import { LiveKitService } from './livekit.service.js'; import { LoggerService } from './logger.service.js'; import { ParticipantNameService } from './participant-name.service.js'; +import { RequestSessionService } from './request-session.service.js'; import { RoomService } from './room.service.js'; import { TokenService } from './token.service.js'; +import { UserService } from './user.service.js'; /** * Service for managing room members and meeting participants. @@ -26,34 +45,186 @@ import { TokenService } from './token.service.js'; export class RoomMemberService { constructor( @inject(LoggerService) protected logger: LoggerService, + @inject(RoomMemberRepository) protected roomMemberRepository: RoomMemberRepository, @inject(RoomService) protected roomService: RoomService, + @inject(UserService) protected userService: UserService, @inject(ParticipantNameService) protected participantNameService: ParticipantNameService, @inject(FrontendEventService) protected frontendEventService: FrontendEventService, @inject(LiveKitService) protected livekitService: LiveKitService, - @inject(TokenService) protected tokenService: TokenService + @inject(TokenService) protected tokenService: TokenService, + @inject(RequestSessionService) protected requestSessionService: RequestSessionService ) {} /** - * Validates a secret against a room's moderator and speaker secrets and returns the corresponding role. + * Creates a new room member. * - * @param roomId - The unique identifier of the room to check - * @param secret - The secret to validate against the room's moderator and speaker secrets - * @returns A promise that resolves to the room member role (MODERATOR or SPEAKER) if the secret is valid - * @throws Error if the moderator or speaker secrets cannot be extracted from their URLs - * @throws Error if the provided secret doesn't match any of the room's secrets (unauthorized) + * @param roomId - The ID of the room + * @param memberOptions - The options for creating the room member + * @returns A promise that resolves to the created MeetRoomMember object */ - async getRoomMemberRoleBySecret(roomId: string, secret: string): Promise { - const room = await this.roomService.getMeetRoom(roomId); - const { moderatorSecret, speakerSecret } = MeetRoomHelper.extractSecretsFromRoom(room); + async createRoomMember(roomId: string, memberOptions: MeetRoomMemberOptions): Promise { + const { userId, name, baseRole, customPermissions } = memberOptions; - switch (secret) { - case moderatorSecret: - return MeetRoomMemberRole.MODERATOR; - case speakerSecret: - return MeetRoomMemberRole.SPEAKER; - default: - throw errorInvalidRoomSecret(room.roomId, secret); + // Generate memberId and member name + let memberId: string; + let memberName: string; + + if (userId) { + // Registered user: memberId = userId, get name from user service + const user = await this.userService.getUser(userId); + + if (!user) { + throw errorUserNotFound(userId); + } + + memberId = userId; + memberName = user.name; + } else if (name) { + // External user: generate memberId, use provided name + memberId = `ext-${secureUid(15)}`; + memberName = name; + } else { + throw new Error('Either userId or name must be provided'); } + + const roomMember = { + memberId, + roomId, + name: memberName, + baseRole, + customPermissions + } as MeetRoomMember; + return this.roomMemberRepository.create(roomMember); + } + + /** + * Checks if a user (registered or external) is a member of a room. + * + * @param roomId - The ID of the room + * @param memberId - The ID of the member + * @returns A promise that resolves to true if the user is a member, false otherwise + */ + async isRoomMember(roomId: string, memberId: string): Promise { + const member = await this.roomMemberRepository.findByRoomAndMemberId(roomId, memberId); + return !!member; + } + + /** + * Retrieves a specific room member by their ID. + * + * @param roomId - The ID of the room + * @param memberId - The ID of the member + * @returns A promise that resolves to the MeetRoomMember object or null if not found + */ + async getRoomMember(roomId: string, memberId: string): Promise { + return this.roomMemberRepository.findByRoomAndMemberId(roomId, memberId); + } + + /** + * Retrieves all members of a room with filtering and pagination. + * + * @param roomId - The ID of the room + * @param filters - Filters for the query + * @returns A promise that resolves to an object containing the members and pagination info + */ + async getAllRoomMembers( + roomId: string, + filters: MeetRoomMemberFilters + ): Promise<{ + members: MeetRoomMember[]; + isTruncated: boolean; + nextPageToken?: string; + }> { + const { fields, ...findOptions } = filters; + const response = await this.roomMemberRepository.findByRoomId(roomId, findOptions); + + if (fields) { + const filteredMembers = response.members.map((member: MeetRoomMember) => + UtilsHelper.filterObjectFields(member, fields) + ); + response.members = filteredMembers as MeetRoomMember[]; + } + + return response; + } + + /** + * Updates an existing room member. + * + * @param roomId - The ID of the room + * @param memberId - The ID of the member to update + * @param updates - The fields to update (baseRole and/or customPermissions) + * @returns A promise that resolves to the updated MeetRoomMember object + */ + async updateRoomMember( + roomId: string, + memberId: string, + updates: { baseRole?: MeetRoomMemberRole; customPermissions?: Partial } + ): Promise { + const member = await this.getRoomMember(roomId, memberId); + + if (!member) { + throw errorRoomMemberNotFound(roomId, memberId); + } + + // Update baseRole if provided + if (updates.baseRole) { + member.baseRole = updates.baseRole; + } + + // Update customPermissions if provided + if (updates.customPermissions) { + member.customPermissions = updates.customPermissions; + } + + return this.roomMemberRepository.update(member); + } + + /** + * Deletes a room member. + * + * @param roomId - The ID of the room + * @param memberId - The ID of the member to delete + */ + async deleteRoomMember(roomId: string, memberId: string): Promise { + const member = await this.getRoomMember(roomId, memberId); + + if (!member) { + throw errorRoomMemberNotFound(roomId, memberId); + } + + return this.roomMemberRepository.deleteByRoomAndMemberId(roomId, memberId); + } + + /** + * Deletes multiple room members in bulk. + * + * @param roomId - The ID of the room + * @param memberIds - Array of member IDs to delete + * @returns A promise that resolves to an object with successful and failed deletions + */ + async bulkDeleteRoomMembers( + roomId: string, + memberIds: string[] + ): Promise<{ + deleted: string[]; + failed: { memberId: string; error: string }[]; + }> { + const membersToDelete = await this.roomMemberRepository.findByRoomAndMemberIds(memberIds); + const foundMemberIds = membersToDelete.map((m) => m.memberId); + + const failed = memberIds + .filter((id) => !foundMemberIds.includes(id)) + .map((id) => ({ memberId: id, error: 'Room member not found' })); + + if (foundMemberIds.length > 0) { + await this.roomMemberRepository.deleteByRoomIdAndMemberIds(roomId, foundMemberIds); + } + + return { + deleted: foundMemberIds, + failed + }; } /** @@ -64,27 +235,93 @@ export class RoomMemberService { * @returns A promise that resolves to the generated token */ async generateOrRefreshRoomMemberToken(roomId: string, tokenOptions: MeetRoomMemberTokenOptions): Promise { - const { secret, grantJoinMeetingPermission = false, participantName, participantIdentity } = tokenOptions; + const { secret, joinMeeting = false, participantName, participantIdentity } = tokenOptions; - // Get room member role from secret - const role = await this.getRoomMemberRoleBySecret(roomId, secret); + let baseRole: MeetRoomMemberRole; + let customPermissions: Partial | undefined = undefined; + let effectivePermissions: MeetRoomMemberPermissions; + let memberId: string | undefined; - if (grantJoinMeetingPermission && participantName) { - return this.generateTokenWithJoinMeetingPermission(roomId, role, participantName, participantIdentity); + if (secret) { + // Case 1: Secret provided (Anonymous access or External Member) + const isValidSecret = await this.roomService.isValidRoomSecret(roomId, secret); + + if (isValidSecret) { + // If secret matches anonymous access URL secret, assign role and permissions based on it + baseRole = await this.getRoomMemberRoleBySecret(roomId, secret); + + const room = await this.roomService.getMeetRoom(roomId); + effectivePermissions = room.roles[baseRole].permissions; + } else { + // If secret is a memberId, fetch the member and assign their role and permissions + const member = await this.getRoomMember(roomId, secret); + + if (member) { + memberId = member.memberId; + baseRole = member.baseRole; + customPermissions = member.customPermissions; + effectivePermissions = member.effectivePermissions; + } else { + throw errorInvalidRoomSecret(roomId, secret); + } + } } else { - return this.generateTokenWithoutJoinMeetingPermission(roomId, role); + // Case 2: Authenticated user + const user = this.requestSessionService.getAuthenticatedUser(); + + if (!user) { + throw errorUnauthorized(); + } + + // Check if user is admin or owner + const isOwner = await this.roomService.isRoomOwner(roomId, user.userId); + + if (user.role === MeetUserRole.ADMIN || isOwner) { + // Admins and owners have MODERATOR role with full permissions + baseRole = MeetRoomMemberRole.MODERATOR; + effectivePermissions = this.getAllPermissions(); + } else { + // If user is a member, fetch their role and permissions + const member = await this.getRoomMember(roomId, user.userId); + + if (member) { + memberId = user.userId; + baseRole = member.baseRole; + customPermissions = member.customPermissions; + effectivePermissions = member.effectivePermissions; + } else { + throw errorUnauthorized(); + } + } + } + + if (joinMeeting && participantName) { + return this.generateTokenForJoiningMeeting( + roomId, + baseRole, + effectivePermissions, + participantName, + participantIdentity, + customPermissions, + memberId + ); + } else { + return this.generateToken(roomId, baseRole, effectivePermissions, customPermissions, memberId); } } /** - * Generates a token with join meeting permissions. + * Generates a token for joining a meeting. * Handles both new token generation and token refresh. */ - protected async generateTokenWithJoinMeetingPermission( + protected async generateTokenForJoiningMeeting( roomId: string, - role: MeetRoomMemberRole, + baseRole: MeetRoomMemberRole, + effectivePermissions: MeetRoomMemberPermissions, participantName: string, - participantIdentity?: string + participantIdentity?: string, + customPermissions?: Partial, + memberId?: string ): Promise { // Check that room is open const room = await this.roomService.getMeetRoom(roomId); @@ -93,12 +330,17 @@ export class RoomMemberService { throw errorRoomClosed(roomId); } + // Check that member has permission to join meeting + if (!effectivePermissions.canJoinMeeting) { + throw errorInsufficientPermissions(); + } + const isRefresh = !!participantIdentity; if (!isRefresh) { // GENERATION MODE this.logger.verbose( - `Generating room member token with join meeting permission for '${participantName}' in room '${roomId}'` + `Generating room member token for joining a meeting for '${participantName}' in room '${roomId}'` ); // Create the Livekit room if it doesn't exist @@ -131,147 +373,132 @@ export class RoomMemberService { } } - // Get participant permissions (with join meeting) - const permissions = await this.getRoomMemberPermissions(roomId, role, true); + const livekitPermissions = await this.getLiveKitPermissions(roomId, effectivePermissions); + const tokenMetadata: MeetRoomMemberTokenMetadata = { + livekitUrl: MEET_ENV.LIVEKIT_URL, + roomId, + memberId, + baseRole, + customPermissions, + effectivePermissions + }; // Generate token with participant name - return this.tokenService.generateRoomMemberToken(role, permissions, participantName, participantIdentity); + return this.tokenService.generateRoomMemberToken( + tokenMetadata, + livekitPermissions, + participantName, + participantIdentity + ); } /** - * Generates a token without join meeting permission. - * This token only provides access to other room resources (recordings, etc.) + * Generates a token for accessing room resources but not joining a meeting. */ - protected async generateTokenWithoutJoinMeetingPermission( + protected async generateToken( roomId: string, - role: MeetRoomMemberRole + baseRole: MeetRoomMemberRole, + effectivePermissions: MeetRoomMemberPermissions, + customPermissions?: Partial, + memberId?: string ): Promise { - this.logger.verbose(`Generating room member token without join meeting permission for room '${roomId}'`); + this.logger.verbose( + `Generating room member token for accessing room resources but not joining a meeting for room '${roomId}'` + ); - // Get participant permissions (without join meeting) - const permissions = await this.getRoomMemberPermissions(roomId, role, false); + const tokenMetadata: MeetRoomMemberTokenMetadata = { + livekitUrl: MEET_ENV.LIVEKIT_URL, + roomId, + memberId, + baseRole, + customPermissions, + effectivePermissions + }; - // Generate token without participant name - return this.tokenService.generateRoomMemberToken(role, permissions); + // Generate token without LiveKit permissions and participant name + return this.tokenService.generateRoomMemberToken(tokenMetadata); } /** - * Gets the permissions for a room member based on their role. + * Validates a secret against a room's moderator and speaker secrets and returns the corresponding role. + * + * @param roomId - The unique identifier of the room to check + * @param secret - The secret to validate against the room's moderator and speaker secrets + * @returns A promise that resolves to the room member role (MODERATOR or SPEAKER) if the secret is valid + * @throws Error if the moderator or speaker secrets cannot be extracted from their URLs + * @throws Error if the provided secret doesn't match any of the room's secrets (unauthorized) + */ + protected async getRoomMemberRoleBySecret(roomId: string, secret: string): Promise { + const room = await this.roomService.getMeetRoom(roomId); + const { moderatorSecret, speakerSecret } = MeetRoomHelper.extractSecretsFromRoom(room); + + switch (secret) { + case moderatorSecret: + return MeetRoomMemberRole.MODERATOR; + case speakerSecret: + return MeetRoomMemberRole.SPEAKER; + default: + throw errorInvalidRoomSecret(room.roomId, secret); + } + } + + /** + * Gets all permissions set to true. + */ + protected getAllPermissions(): MeetRoomMemberPermissions { + return { + canRecord: true, + canRetrieveRecordings: true, + canDeleteRecordings: true, + canJoinMeeting: true, + canShareAccessLinks: true, + canMakeModerator: true, + canKickParticipants: true, + canEndMeeting: true, + canPublishVideo: true, + canPublishAudio: true, + canShareScreen: true, + canReadChat: true, + canWriteChat: true, + canChangeVirtualBackground: true + }; + } + + /** + * Gets the LiveKit permissions for a room member based on their Meet permissions. * * @param roomId - The ID of the room - * @param role - The role of the room member - * @param addJoinPermission - Whether to include join permission (for meeting access) - * @returns The permissions for the room member + * @returns The LiveKit permissions for the room member */ - async getRoomMemberPermissions( + protected async getLiveKitPermissions( roomId: string, - role: MeetRoomMemberRole, - addJoinPermission = true - ): Promise { - const recordingPermissions = await this.getRecordingPermissions(roomId, role); + permissions: MeetRoomMemberPermissions + ): Promise { + const canPublishSources: TrackSource[] = []; - switch (role) { - case MeetRoomMemberRole.MODERATOR: - return this.generateModeratorPermissions( - roomId, - recordingPermissions.canRetrieveRecordings, - recordingPermissions.canDeleteRecordings, - addJoinPermission - ); - case MeetRoomMemberRole.SPEAKER: - return this.generateSpeakerPermissions( - roomId, - recordingPermissions.canRetrieveRecordings, - recordingPermissions.canDeleteRecordings, - addJoinPermission - ); - } - } - - protected generateModeratorPermissions( - roomId: string, - canRetrieveRecordings: boolean, - canDeleteRecordings: boolean, - addJoinPermission: boolean - ): MeetRoomMemberPermissions { - return { - livekit: { - roomJoin: addJoinPermission, - room: roomId, - canPublish: true, - canSubscribe: true, - canPublishData: true, - canUpdateOwnMetadata: true - }, - meet: { - canRecord: true, - canRetrieveRecordings, - canDeleteRecordings, - canChat: true, - canChangeVirtualBackground: true - } - }; - } - - protected generateSpeakerPermissions( - roomId: string, - canRetrieveRecordings: boolean, - canDeleteRecordings: boolean, - addJoinPermission: boolean - ): MeetRoomMemberPermissions { - return { - livekit: { - roomJoin: addJoinPermission, - room: roomId, - canPublish: true, - canSubscribe: true, - canPublishData: true, - canUpdateOwnMetadata: true - }, - meet: { - canRecord: false, - canRetrieveRecordings, - canDeleteRecordings, - canChat: true, - canChangeVirtualBackground: true - } - }; - } - - protected async getRecordingPermissions( - roomId: string, - role: MeetRoomMemberRole - ): Promise<{ - canRetrieveRecordings: boolean; - canDeleteRecordings: boolean; - }> { - const room = await this.roomService.getMeetRoom(roomId); - const recordingAccess = room.config.recording.allowAccessTo; - - if (!recordingAccess) { - // Default to no access if not configured - return { - canRetrieveRecordings: false, - canDeleteRecordings: false - }; + if (permissions.canPublishAudio) { + canPublishSources.push(TrackSource.MICROPHONE); } - // A room member can delete recordings if they are a moderator and the recording access is not set to admin - const canDeleteRecordings = - role === MeetRoomMemberRole.MODERATOR && recordingAccess !== MeetRecordingAccess.ADMIN; + if (permissions.canPublishVideo) { + canPublishSources.push(TrackSource.CAMERA); + } - /* A room member can retrieve recordings if - - they can delete recordings - - they are a speaker and the recording access includes speakers - */ - const canRetrieveRecordings = - canDeleteRecordings || - (role === MeetRoomMemberRole.SPEAKER && recordingAccess === MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER); + if (permissions.canShareScreen) { + canPublishSources.push(TrackSource.SCREEN_SHARE); + canPublishSources.push(TrackSource.SCREEN_SHARE_AUDIO); + } - return { - canRetrieveRecordings, - canDeleteRecordings + const livekitPermissions: LiveKitPermissions = { + room: roomId, + roomJoin: true, + canPublish: permissions.canPublishAudio || permissions.canPublishVideo || permissions.canShareScreen, + canPublishSources, + canSubscribe: true, + canPublishData: true, + canUpdateOwnMetadata: true }; + return livekitPermissions; } /** @@ -304,9 +531,9 @@ export class RoomMemberService { const metadata: MeetRoomMemberTokenMetadata = this.parseRoomMemberTokenMetadata(participant.metadata); // Update role and permissions in metadata - metadata.role = newRole; - const { meet } = await this.getRoomMemberPermissions(roomId, newRole); - metadata.permissions = meet; + metadata.baseRole = newRole; + metadata.customPermissions = undefined; + metadata.effectivePermissions = meetRoom.roles[newRole].permissions; await this.livekitService.updateParticipantMetadata(roomId, participantIdentity, JSON.stringify(metadata)); diff --git a/meet-ce/backend/src/services/token.service.ts b/meet-ce/backend/src/services/token.service.ts index 32532458..db223b50 100644 --- a/meet-ce/backend/src/services/token.service.ts +++ b/meet-ce/backend/src/services/token.service.ts @@ -1,9 +1,4 @@ -import { - MeetRoomMemberPermissions, - MeetRoomMemberRole, - MeetRoomMemberTokenMetadata, - MeetUser -} from '@openvidu-meet/typings'; +import { LiveKitPermissions, MeetRoomMemberTokenMetadata, MeetUser } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { jwtDecode } from 'jwt-decode'; import { AccessToken, AccessTokenOptions, ClaimGrants, TokenVerifier, VideoGrant } from 'livekit-server-sdk'; @@ -17,10 +12,11 @@ export class TokenService { async generateAccessToken(user: MeetUser): Promise { const tokenOptions: AccessTokenOptions = { - identity: user.username, + identity: user.userId, + name: user.name, ttl: INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION, metadata: JSON.stringify({ - roles: user.roles + role: user.role }) }; return await this.generateJwtToken(tokenOptions); @@ -28,34 +24,29 @@ export class TokenService { async generateRefreshToken(user: MeetUser): Promise { const tokenOptions: AccessTokenOptions = { - identity: user.username, + identity: user.userId, + name: user.name, ttl: INTERNAL_CONFIG.REFRESH_TOKEN_EXPIRATION, metadata: JSON.stringify({ - roles: user.roles + role: user.role }) }; return await this.generateJwtToken(tokenOptions); } async generateRoomMemberToken( - role: MeetRoomMemberRole, - permissions: MeetRoomMemberPermissions, + tokenMetadata: MeetRoomMemberTokenMetadata, + livekitPermissions?: LiveKitPermissions, participantName?: string, participantIdentity?: string ): Promise { - const metadata: MeetRoomMemberTokenMetadata = { - livekitUrl: MEET_ENV.LIVEKIT_URL, - role, - permissions: permissions.meet - }; - const tokenOptions: AccessTokenOptions = { identity: participantIdentity, name: participantName, ttl: INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION, - metadata: JSON.stringify(metadata) + metadata: JSON.stringify(tokenMetadata) }; - return await this.generateJwtToken(tokenOptions, permissions.livekit as VideoGrant); + return await this.generateJwtToken(tokenOptions, livekitPermissions); } private async generateJwtToken(tokenOptions: AccessTokenOptions, grants?: VideoGrant): Promise {