From 1223e3d53bd99759e8fcdc989bbd1339ba07a8fb Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 6 Mar 2026 11:39:28 +0100 Subject: [PATCH] feat: enhance room member token handling with participant metadata and moderation actions Enhances token refresh with participant metadata support Improves room member token generation to allow rebuilding token metadata from current participant state in LiveKit, enabling accurate permission and role handling after in-meeting upgrades or downgrades. Adds support for in-meeting moderation actions (promote/demote moderator) and updates token and context logic to reflect dynamic role and permission changes for participants. --- meet-ce/backend/src/models/token.model.ts | 2 - .../models/zod-schemas/room-member.schema.ts | 13 +- .../src/services/room-member.service.ts | 464 +++++++++++------- .../room-member-error-handler.service.ts | 5 +- .../services/room-member-context.service.ts | 23 +- .../src/request/room-member-request.ts | 15 + .../src/response/room-member-response.ts | 27 +- 7 files changed, 338 insertions(+), 211 deletions(-) diff --git a/meet-ce/backend/src/models/token.model.ts b/meet-ce/backend/src/models/token.model.ts index 1960438d..cc35e786 100644 --- a/meet-ce/backend/src/models/token.model.ts +++ b/meet-ce/backend/src/models/token.model.ts @@ -34,6 +34,4 @@ export interface MeetRoomMemberTokenOptions { participantName?: string; /** Identity of the participant */ participantIdentity?: string; - /** Indicates if the room has captions enabled */ - roomWithCaptions?: boolean; } diff --git a/meet-ce/backend/src/models/zod-schemas/room-member.schema.ts b/meet-ce/backend/src/models/zod-schemas/room-member.schema.ts index d0256ad9..c85c5228 100644 --- a/meet-ce/backend/src/models/zod-schemas/room-member.schema.ts +++ b/meet-ce/backend/src/models/zod-schemas/room-member.schema.ts @@ -7,6 +7,7 @@ import { MeetRoomMemberRole, MeetRoomMemberTokenMetadata, MeetRoomMemberTokenOptions, + MeetRoomMemberUIBadge, SortOrder } from '@openvidu-meet/typings'; import { z } from 'zod'; @@ -141,7 +142,8 @@ export const RoomMemberTokenOptionsSchema: z.ZodType secret: z.string().optional(), joinMeeting: z.boolean().optional().default(false), participantName: z.string().optional(), - participantIdentity: z.string().optional() + participantIdentity: z.string().optional(), + useParticipantMetadata: z.boolean().optional().default(false) }) .refine( (data) => { @@ -156,10 +158,11 @@ export const RoomMemberTokenOptionsSchema: z.ZodType export const RoomMemberTokenMetadataSchema: z.ZodType = z.object({ iat: z.number(), - livekitUrl: z.string().url('LiveKit URL must be a valid URL'), roomId: z.string(), memberId: z.string().optional(), - baseRole: RoomMemberRoleSchema, - customPermissions: PartialMeetPermissionsSchema.optional(), - effectivePermissions: MeetPermissionsSchema + userId: z.string().optional(), + permissions: MeetPermissionsSchema, + badge: z.nativeEnum(MeetRoomMemberUIBadge), + isPromotedModerator: z.boolean().optional(), + livekitUrl: z.string().url('LiveKit URL must be a valid URL').optional() }); diff --git a/meet-ce/backend/src/services/room-member.service.ts b/meet-ce/backend/src/services/room-member.service.ts index 855deede..eb14b215 100644 --- a/meet-ce/backend/src/services/room-member.service.ts +++ b/meet-ce/backend/src/services/room-member.service.ts @@ -1,5 +1,6 @@ import { LiveKitPermissions, + MeetParticipantModerationAction, MeetRoomMember, MeetRoomMemberField, MeetRoomMemberFilters, @@ -8,6 +9,7 @@ import { MeetRoomMemberRole, MeetRoomMemberTokenMetadata, MeetRoomMemberTokenOptions, + MeetRoomMemberUIBadge, MeetRoomRoles, MeetRoomStatus, MeetUserRole, @@ -43,6 +45,17 @@ import { RoomService } from './room.service.js'; import { TokenService } from './token.service.js'; import { UserService } from './user.service.js'; +interface ResolvedPermissionSource { + memberId?: string; + userId?: string; + permissions: MeetRoomMemberPermissions; + badge?: MeetRoomMemberUIBadge; +} + +interface ParticipantMeetingMetadata extends MeetRoomMemberTokenMetadata { + originalPermissions?: MeetRoomMemberPermissions; +} + /** * Service for managing room members and meeting participants. */ @@ -391,125 +404,59 @@ export class RoomMemberService { * @returns A promise that resolves to the generated token */ async generateOrRefreshRoomMemberToken(roomId: string, tokenOptions: MeetRoomMemberTokenOptions): Promise { - const { secret, joinMeeting = false, participantName, participantIdentity } = tokenOptions; + const { + secret, + joinMeeting = false, + participantName, + participantIdentity, + useParticipantMetadata = false + } = tokenOptions; - let baseRole: MeetRoomMemberRole; - 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) - const isExternalMemberId = secret.startsWith('ext-'); - - if (isExternalMemberId) { - // If secret is a external member ID, fetch the member and assign their role and permissions - const member = await this.getRoomMember(roomId, secret); - - if (!member) { - throw errorRoomMemberNotFound(roomId, secret); - } - - memberId = member.memberId; - baseRole = member.baseRole; - customPermissions = member.customPermissions; - effectivePermissions = member.effectivePermissions; - } else { - // If secret matches anonymous access URL secret, assign role and permissions based on it - const anonymousAccess = await this.resolveAnonymousAccessBySecret(roomId, secret); - baseRole = anonymousAccess.baseRole; - effectivePermissions = anonymousAccess.effectivePermissions; - } - } else { - // Case 2: Authenticated user - const user = this.requestSessionService.getAuthenticatedUser(); - - if (!user) { + if (joinMeeting && participantName && useParticipantMetadata) { + if (!participantIdentity) { + // TODO: Consider throwing a more specific error indicating that participant identity is required for token refresh when using participant metadata throw errorUnauthorized(); } - userId = user.userId; - - // 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) { - throw errorInsufficientPermissions(); - } - - memberId = user.userId; - baseRole = member.baseRole; - customPermissions = member.customPermissions; - effectivePermissions = member.effectivePermissions; - } + const tokenMetadata = await this.resolveTokenMetadataFromParticipant(roomId, participantIdentity); + return this.generateTokenForJoiningMeeting(roomId, tokenMetadata, participantName, participantIdentity); } + const [secretSource, authenticatedSource, room] = await Promise.all([ + secret ? this.resolvePermissionSourceFromSecret(roomId, secret) : Promise.resolve(undefined), + this.resolvePermissionSourceFromAuthenticatedUser(roomId), + this.roomService.getMeetRoom(roomId, ['roles']) + ]); + + if (!secretSource && !authenticatedSource) { + throw errorUnauthorized(); + } + + const mergedPermissions = this.mergePermissions(secretSource?.permissions, authenticatedSource?.permissions); + let badge = authenticatedSource?.badge; + + if (!badge) { + badge = this.resolveBadgeFromPermissions(mergedPermissions, room.roles.moderator.permissions); + } + + const tokenMetadata: MeetRoomMemberTokenMetadata = { + iat: Date.now(), + roomId, + memberId: secretSource?.memberId || authenticatedSource?.memberId, + userId: authenticatedSource?.userId, + permissions: mergedPermissions, + badge + }; + if (joinMeeting && participantName) { - return this.generateTokenForJoiningMeeting( - roomId, - baseRole, - effectivePermissions, - participantName, - participantIdentity, - customPermissions, - memberId, - userId - ); + tokenMetadata.livekitUrl = MEET_ENV.LIVEKIT_URL; + return this.generateTokenForJoiningMeeting(roomId, tokenMetadata, participantName, participantIdentity); } - return this.generateToken(roomId, baseRole, effectivePermissions, customPermissions, memberId); - } - - /** - * Resolves anonymous access and effective permissions from a room secret. - * - * - Moderator and speaker secrets map to their room role permissions. - * - Recording secret maps to read-only recording permissions. - */ - protected async resolveAnonymousAccessBySecret( - roomId: string, - secret: string - ): Promise<{ baseRole: MeetRoomMemberRole; effectivePermissions: MeetRoomMemberPermissions }> { - const { roles, access } = await this.roomService.getMeetRoom(roomId, ['roles', 'access']); - const { moderatorSecret, speakerSecret, recordingSecret } = MeetRoomHelper.extractSecretsFromRoom(access); - - const anonymousRole: MeetRoomMemberRole | 'recording' | undefined = - secret === moderatorSecret - ? MeetRoomMemberRole.MODERATOR - : secret === speakerSecret - ? MeetRoomMemberRole.SPEAKER - : secret === recordingSecret - ? 'recording' - : undefined; - - if (!anonymousRole) { - throw errorInvalidRoomSecret(roomId, secret); - } - - if (!access.anonymous[anonymousRole].enabled) { - throw errorAnonymousAccessDisabled(roomId, anonymousRole); - } - - if (anonymousRole === 'recording') { - return { - baseRole: MeetRoomMemberRole.SPEAKER, - effectivePermissions: this.getRecordingReadOnlyPermissions() - }; - } - - return { - baseRole: anonymousRole, - effectivePermissions: roles[anonymousRole].permissions - }; + this.logger.verbose( + `Generating room member token for accessing room resources but not joining a meeting for room '${roomId}'` + ); + return this.tokenService.generateRoomMemberToken({ tokenMetadata }); } /** @@ -518,23 +465,19 @@ export class RoomMemberService { */ protected async generateTokenForJoiningMeeting( roomId: string, - baseRole: MeetRoomMemberRole, - effectivePermissions: MeetRoomMemberPermissions, + tokenMetadata: MeetRoomMemberTokenMetadata, participantName: string, - participantIdentity?: string, - customPermissions?: Partial, - memberId?: string, - userId?: string + participantIdentity?: string ): Promise { // Check that room is open - const { status, config } = await this.roomService.getMeetRoom(roomId, ['status', 'config']); + const { status } = await this.roomService.getMeetRoom(roomId, ['status']); if (status === MeetRoomStatus.CLOSED) { throw errorRoomClosed(roomId); } // Check that member has permission to join meeting - if (!effectivePermissions.canJoinMeeting) { + if (!tokenMetadata.permissions.canJoinMeeting) { throw errorInsufficientPermissions(); } @@ -558,11 +501,11 @@ export class RoomMemberService { // Create the Livekit room if it doesn't exist await this.roomService.createLivekitRoom(roomId); - if (memberId || userId) { + if (tokenMetadata.memberId || tokenMetadata.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; + participantIdentity = tokenMetadata.memberId || tokenMetadata.userId; } else { // For anonymous users, create a unique participant identity based on the provided participant name const identityPrefix = this.createParticipantIdentityPrefixFromName(participantName) || 'participant'; @@ -583,56 +526,166 @@ export class RoomMemberService { } } - const livekitPermissions = this.getLiveKitPermissions(roomId, effectivePermissions); - const tokenMetadata: MeetRoomMemberTokenMetadata = { - iat: Date.now(), - livekitUrl: MEET_ENV.LIVEKIT_URL, - roomId, - memberId, - baseRole, - customPermissions, - effectivePermissions - }; - const roomWithCaptions = config.captions.enabled; - - // Generate token with participant name + const livekitPermissions = this.getLiveKitPermissions(roomId, tokenMetadata.permissions); return this.tokenService.generateRoomMemberToken({ tokenMetadata, livekitPermissions, participantName, - participantIdentity, - roomWithCaptions + participantIdentity }); } - /** - * Generates a token for accessing room resources but not joining a meeting. - */ - protected async generateToken( + protected async resolvePermissionSourceFromSecret( roomId: string, - baseRole: MeetRoomMemberRole, - effectivePermissions: MeetRoomMemberPermissions, - customPermissions?: Partial, - memberId?: string - ): Promise { - this.logger.verbose( - `Generating room member token for accessing room resources but not joining a meeting for room '${roomId}'` + secret: string + ): Promise { + const isExternalMemberId = secret.startsWith('ext-'); + + if (isExternalMemberId) { + const member = await this.getRoomMember(roomId, secret); + + if (!member) { + throw errorRoomMemberNotFound(roomId, secret); + } + + return { + memberId: member.memberId, + permissions: member.effectivePermissions + }; + } + + return this.resolveAnonymousAccessBySecret(roomId, secret); + } + + /** + * Resolves anonymous access and effective permissions from a room secret. + * + * - Moderator and speaker secrets map to their room role permissions. + * - Recording secret maps to read-only recording permissions. + */ + protected async resolveAnonymousAccessBySecret(roomId: string, secret: string): Promise { + const { roles, access } = await this.roomService.getMeetRoom(roomId, ['roles', 'access']); + const { moderatorSecret, speakerSecret, recordingSecret } = MeetRoomHelper.extractSecretsFromRoom(access); + + const anonymousRole: MeetRoomMemberRole | 'recording' | undefined = + secret === moderatorSecret + ? MeetRoomMemberRole.MODERATOR + : secret === speakerSecret + ? MeetRoomMemberRole.SPEAKER + : secret === recordingSecret + ? 'recording' + : undefined; + + if (!anonymousRole) { + throw errorInvalidRoomSecret(roomId, secret); + } + + if (!access.anonymous[anonymousRole].enabled) { + throw errorAnonymousAccessDisabled(roomId, anonymousRole); + } + + if (anonymousRole === 'recording') { + return { + permissions: this.getRecordingReadOnlyPermissions() + }; + } + + return { + permissions: roles[anonymousRole].permissions + }; + } + + protected async resolvePermissionSourceFromAuthenticatedUser( + roomId: string + ): Promise { + const user = this.requestSessionService.getAuthenticatedUser(); + + if (!user) { + return undefined; + } + + const isAdmin = user.role === MeetUserRole.ADMIN; + const isOwner = await this.roomService.isRoomOwner(roomId, user.userId); + + if (isAdmin || isOwner) { + // Admins and room owner get all permissions without needing a member record in the database + return { + userId: user.userId, + permissions: this.getAllPermissions(), + badge: isAdmin ? MeetRoomMemberUIBadge.ADMIN : MeetRoomMemberUIBadge.OWNER + }; + } + + const member = await this.getRoomMember(roomId, user.userId); + + if (!member) { + return undefined; + } + + return { + memberId: user.userId, + userId: user.userId, + permissions: member.effectivePermissions + }; + } + + protected mergePermissions( + first?: MeetRoomMemberPermissions, + second?: MeetRoomMemberPermissions + ): MeetRoomMemberPermissions { + const merged = this.getNoPermissions(); + + const sources = [first, second].filter( + (permissions): permissions is MeetRoomMemberPermissions => !!permissions ); - const tokenMetadata: MeetRoomMemberTokenMetadata = { - iat: Date.now(), - livekitUrl: MEET_ENV.LIVEKIT_URL, - roomId, - memberId, - baseRole, - customPermissions, - effectivePermissions - }; + for (const source of sources) { + for (const [key, value] of Object.entries(source) as [keyof MeetRoomMemberPermissions, boolean][]) { + merged[key] = merged[key] || value; + } + } - // Generate token without LiveKit permissions and participant name - return this.tokenService.generateRoomMemberToken({ - tokenMetadata + return merged; + } + + protected resolveBadgeFromPermissions( + permissions: MeetRoomMemberPermissions, + moderatorPermissions: MeetRoomMemberPermissions + ): MeetRoomMemberUIBadge { + const hasModeratorPermissions = Object.entries(moderatorPermissions).every(([permission, allowed]) => { + if (!allowed) { + return true; + } + + return permissions[permission as keyof MeetRoomMemberPermissions] === true; }); + + return hasModeratorPermissions ? MeetRoomMemberUIBadge.MODERATOR : MeetRoomMemberUIBadge.OTHER; + } + + protected async resolveTokenMetadataFromParticipant( + roomId: string, + participantIdentity: string + ): Promise { + const sessionParticipantIdentity = this.requestSessionService.getParticipantIdentity(); + + if (!sessionParticipantIdentity || sessionParticipantIdentity !== participantIdentity) { + throw errorInsufficientPermissions(); + } + + const participant = await this.getParticipantFromMeeting(roomId, participantIdentity); + const participantMetadata = this.parseParticipantMeetingMetadata(participant.metadata); + + return { + iat: Date.now(), + roomId, + memberId: participantMetadata.memberId, + userId: participantMetadata.userId, + permissions: participantMetadata.permissions, + badge: participantMetadata.badge, + isPromotedModerator: participantMetadata.isPromotedModerator, + livekitUrl: participantMetadata.livekitUrl + }; } /** @@ -723,6 +776,58 @@ export class RoomMemberService { return livekitPermissions; } + async updateParticipantRole( + roomId: string, + participantIdentity: string, + action: MeetParticipantModerationAction + ): Promise { + try { + const { roles } = await this.roomService.getMeetRoom(roomId, ['roles']); + const participant = await this.getParticipantFromMeeting(roomId, participantIdentity); + const metadata = this.parseParticipantMeetingMetadata(participant.metadata); + + if (action === MeetParticipantModerationAction.UPGRADE) { + if (metadata.badge !== MeetRoomMemberUIBadge.OTHER) { + // TODO: Consider throwing a more specific error indicating that only participants with OTHER badge can be promoted to moderator + throw errorInsufficientPermissions(); + } + + metadata.originalPermissions = metadata.permissions; + metadata.permissions = this.mergePermissions( + metadata.permissions, + roles[MeetRoomMemberRole.MODERATOR].permissions + ); + metadata.badge = MeetRoomMemberUIBadge.MODERATOR; + metadata.isPromotedModerator = true; + } else { + if ( + metadata.badge !== MeetRoomMemberUIBadge.MODERATOR || + !metadata.isPromotedModerator || + !metadata.originalPermissions + ) { + // TODO: Consider throwing a more specific error indicating that only participants with MODERATOR badge + // that were promoted (not original moderators) can be demoted back to their original permissions + throw errorInsufficientPermissions(); + } + + metadata.permissions = metadata.originalPermissions; + metadata.badge = MeetRoomMemberUIBadge.OTHER; + metadata.isPromotedModerator = undefined; + delete metadata.originalPermissions; + } + + await this.livekitService.updateParticipantMetadata(roomId, participantIdentity, JSON.stringify(metadata)); + await this.frontendEventService.sendParticipantRoleUpdatedSignal( + roomId, + participantIdentity, + metadata.badge + ); + } catch (error) { + this.logger.error('Error applying participant moderation action:', error); + throw error; + } + } + /** * Kicks multiple members from a meeting in batches. * This method processes the kicks in parallel batches to avoid overwhelming the system. @@ -795,39 +900,6 @@ export class RoomMemberService { return this.livekitService.deleteParticipant(roomId, participantIdentity); } - async updateParticipantRole( - roomId: string, - participantIdentity: string, - newRole: MeetRoomMemberRole - ): Promise { - try { - const { roles, access } = await this.roomService.getMeetRoom(roomId, ['roles', 'access']); - const participant = await this.getParticipantFromMeeting(roomId, participantIdentity); - const metadata: MeetRoomMemberTokenMetadata = this.tokenService.parseRoomMemberTokenMetadata( - participant.metadata - ); - - // Update role and permissions in metadata - metadata.baseRole = newRole; - metadata.customPermissions = undefined; - metadata.effectivePermissions = roles[newRole].permissions; - - await this.livekitService.updateParticipantMetadata(roomId, participantIdentity, JSON.stringify(metadata)); - - const { speakerSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(access); - const secret = newRole === MeetRoomMemberRole.MODERATOR ? moderatorSecret : speakerSecret; - await this.frontendEventService.sendParticipantRoleUpdatedSignal( - roomId, - participantIdentity, - newRole, - secret - ); - } catch (error) { - this.logger.error('Error updating participant role:', error); - throw error; - } - } - protected async existsParticipantInMeeting(roomId: string, participantIdentity: string): Promise { this.logger.verbose(`Checking if participant '${participantIdentity}' exists in room '${roomId}'`); return this.livekitService.participantExists(roomId, participantIdentity); @@ -838,6 +910,16 @@ export class RoomMemberService { return this.livekitService.getParticipant(roomId, participantIdentity); } + protected parseParticipantMeetingMetadata(metadata: string): ParticipantMeetingMetadata { + const parsed = JSON.parse(metadata || '{}') as ParticipantMeetingMetadata; + const normalized = this.tokenService.parseRoomMemberTokenMetadata(JSON.stringify(parsed)); + + return { + ...parsed, + ...normalized + }; + } + /** * Creates a sanitized participant identity prefix from the given participant name. * diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/room-members/interceptor-handlers/room-member-error-handler.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/room-members/interceptor-handlers/room-member-error-handler.service.ts index 92f09724..a0d0878b 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/room-members/interceptor-handlers/room-member-error-handler.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/room-members/interceptor-handlers/room-member-error-handler.service.ts @@ -75,13 +75,16 @@ export class RoomMemberInterceptorErrorHandlerService implements HttpErrorHandle const participantName = this.roomMemberContextService.participantName(); const participantIdentity = this.roomMemberContextService.participantIdentity(); const joinMeeting = !!participantIdentity; // Grant join permission if identity is set + // If the member is a promoted moderator, we need to use LiveKit participant metadata to get the updated permissions in the token + const useParticipantMetadata = this.roomMemberContextService.isPromotedModerator(); return from( this.roomMemberContextService.generateToken(roomId, { secret, joinMeeting, participantName, - participantIdentity + participantIdentity, + useParticipantMetadata }) ).pipe( switchMap(() => { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/room-members/services/room-member-context.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/room-members/services/room-member-context.service.ts index 76b4ad37..5970d853 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/room-members/services/room-member-context.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/room-members/services/room-member-context.service.ts @@ -3,7 +3,8 @@ import { MeetRoomMember, MeetRoomMemberPermissions, MeetRoomMemberTokenMetadata, - MeetRoomMemberTokenOptions + MeetRoomMemberTokenOptions, + MeetRoomMemberUIBadge } from '@openvidu-meet/typings'; import { E2eeService, LoggerService } from 'openvidu-components-angular'; import { TokenStorageService } from '../../../shared/services/token-storage.service'; @@ -23,6 +24,8 @@ export class RoomMemberContextService { private readonly _participantName = signal(undefined); private readonly _isParticipantNameFromUrl = signal(false); private readonly _participantIdentity = signal(undefined); + private readonly _memberBadge = signal(MeetRoomMemberUIBadge.OTHER); + private readonly _isPromotedModerator = signal(false); private readonly _permissions = signal(undefined); private readonly _member = signal(undefined); @@ -40,6 +43,10 @@ export class RoomMemberContextService { readonly member = this._member.asReadonly(); /** Computed signal for the room member's display name */ readonly memberName = computed(() => this._member()?.name); + /** Readonly signal for the room member's UI badge */ + readonly memberBadge = this._memberBadge.asReadonly(); + /** Readonly signal for whether the room member is a promoted moderator */ + readonly isPromotedModerator = this._isPromotedModerator.asReadonly(); protected log; @@ -83,6 +90,14 @@ export class RoomMemberContextService { } } + /** + * Sets the promoted moderator status for the current room member. + * @param isPromoted - A boolean indicating whether the member has been promoted to moderator status. + */ + setPromotedModerator(isPromoted: boolean): void { + this._isPromotedModerator.set(isPromoted); + } + /** * Checks if the current room member has a specific permission. * @@ -132,7 +147,8 @@ export class RoomMemberContextService { this._participantIdentity.set(decodedToken.sub); } - this._permissions.set(metadata.effectivePermissions); + this._permissions.set(metadata.permissions); + this._memberBadge.set(metadata.badge); // If token contains memberId, fetch and store member info if (metadata.memberId) { @@ -143,7 +159,6 @@ export class RoomMemberContextService { this.log.w('Could not fetch member info:', error); } } - } catch (error) { this.log.e('Error decoding room member token:', error); throw new Error('Invalid room member token'); @@ -159,6 +174,8 @@ export class RoomMemberContextService { this._isParticipantNameFromUrl.set(false); this._participantIdentity.set(undefined); this._permissions.set(undefined); + this._memberBadge.set(MeetRoomMemberUIBadge.OTHER); + this._isPromotedModerator.set(false); this._member.set(undefined); } } diff --git a/meet-ce/typings/src/request/room-member-request.ts b/meet-ce/typings/src/request/room-member-request.ts index ebdc05aa..700a686d 100644 --- a/meet-ce/typings/src/request/room-member-request.ts +++ b/meet-ce/typings/src/request/room-member-request.ts @@ -39,4 +39,19 @@ export interface MeetRoomMemberTokenOptions { * Required when refreshing an existing token used to join a meeting. */ participantIdentity?: string; + /** + * If true, the token metadata is rebuilt from the current participant metadata in LiveKit. + * Used when refreshing tokens after in-meeting permission updates. + */ + useParticipantMetadata?: boolean; +} + +/** + * Enum representing moderation actions that can be performed on a meeting participant. + */ +export enum MeetParticipantModerationAction { + /** Action to promote a participant to moderator role */ + UPGRADE = 'upgrade', + /** Action to demote a participant from moderator role and revert to original role */ + DOWNGRADE = 'downgrade' } diff --git a/meet-ce/typings/src/response/room-member-response.ts b/meet-ce/typings/src/response/room-member-response.ts index db171a6d..08cb1b58 100644 --- a/meet-ce/typings/src/response/room-member-response.ts +++ b/meet-ce/typings/src/response/room-member-response.ts @@ -1,5 +1,5 @@ import { MeetRoomMemberPermissions } from '../database/room-member-permissions.js'; -import { MeetRoomMember, MeetRoomMemberRole } from '../database/room-member.entity.js'; +import { MeetRoomMember } from '../database/room-member.entity.js'; import { SortAndPagination, SortableFieldKey } from './sort-pagination.js'; /** @@ -53,23 +53,32 @@ export interface MeetRoomMemberFilters extends SortAndPagination; - /** Effective permissions for the member (combination of base role and custom permissions). See {@link MeetRoomMemberPermissions} for details. */ - effectivePermissions: MeetRoomMemberPermissions; + /** Unique identifier for the user if defined */ + userId?: string; + /** Effective permissions for the member. */ + permissions: MeetRoomMemberPermissions; + /** Visual badge/category used in participant UI. */ + badge: MeetRoomMemberUIBadge; + /** Indicates if participant has been promoted to moderator during the meeting and is not originally a moderator. */ + isPromotedModerator?: boolean; + /** URL of the LiveKit server to connect to when joining the meeting */ + livekitUrl?: string; } +/** + * UI badge/category for room members, used to visually distinguish roles in the participant list and other UI elements. + */ export enum MeetRoomMemberUIBadge { + /** Owner badge, typically for the creator of the room */ OWNER = 'owner', + /** Admin badge, typically for users with administrative privileges */ ADMIN = 'admin', + /** Moderator badge, typically for users with moderation privileges */ MODERATOR = 'moderator', + /** Other badge, typically for regular participants without special privileges */ OTHER = 'other' }