backend: implement room member management features including CRUD operations

This commit is contained in:
juancarmore 2025-12-16 17:29:45 +01:00
parent a26f2a754b
commit 93046c8db0
5 changed files with 532 additions and 185 deletions

View File

@ -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);

View File

@ -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 => {

View File

@ -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<MeetRoomMember, MeetRoo
return domain;
}
/**
* Finds room members by their memberIds.
*
* @param memberIds - Array of member identifiers
* @returns Array of found room members
*/
async findByRoomAndMemberIds(memberIds: string[]): Promise<MeetRoomMember[]> {
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<MeetRoomMember, MeetRoo
await this.deleteMany({ roomId, memberId: { $in: memberIds } });
}
/**
* 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<void> {
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<void> {
await this.deleteMany({ roomId });
}
// ==========================================
// PRIVATE HELPER METHODS

View File

@ -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<MeetRoomMemberRole> {
const room = await this.roomService.getMeetRoom(roomId);
const { moderatorSecret, speakerSecret } = MeetRoomHelper.extractSecretsFromRoom(room);
async createRoomMember(roomId: string, memberOptions: MeetRoomMemberOptions): Promise<MeetRoomMember> {
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<boolean> {
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<MeetRoomMember | null> {
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<MeetRoomMemberPermissions> }
): Promise<MeetRoomMember> {
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<void> {
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<string> {
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<MeetRoomMemberPermissions> | 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<MeetRoomMemberPermissions>,
memberId?: string
): Promise<string> {
// 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<MeetRoomMemberPermissions>,
memberId?: string
): Promise<string> {
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<MeetRoomMemberRole> {
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<MeetRoomMemberPermissions> {
const recordingPermissions = await this.getRecordingPermissions(roomId, role);
permissions: MeetRoomMemberPermissions
): Promise<LiveKitPermissions> {
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));

View File

@ -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<string> {
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<string> {
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<string> {
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<string> {