frontend: move room member related services to dedicated model directory. Implement RoomMemberService for managing room members API

This commit is contained in:
juancarmore 2026-02-07 00:00:45 +01:00
parent ded3c37ab2
commit f61fa6183c
19 changed files with 238 additions and 70 deletions

View File

@ -17,7 +17,7 @@ import { FeatureConfigurationService } from '../../../../shared/services/feature
import { GlobalConfigService } from '../../../../shared/services/global-config.service';
import { NotificationService } from '../../../../shared/services/notification.service';
import { SoundService } from '../../../../shared/services/sound.service';
import { RoomMemberService } from '../../../rooms/services/room-member.service';
import { RoomMemberContextService } from '../../../room-members/services/room-member-context.service';
import { MeetingLobbyComponent } from '../../components/meeting-lobby/meeting-lobby.component';
import { MeetingParticipantItemComponent } from '../../customization/meeting-participant-item/meeting-participant-item.component';
import { MeetingCaptionsService } from '../../services/meeting-captions.service';
@ -63,7 +63,7 @@ export class MeetingComponent implements OnInit {
protected isMeetingLeft = signal(false);
protected features: Signal<ApplicationFeatures>;
protected participantService = inject(RoomMemberService);
protected participantService = inject(RoomMemberContextService);
protected featureConfService = inject(FeatureConfigurationService);
protected ovThemeService = inject(OpenViduThemeService);
protected configService = inject(GlobalConfigService);

View File

@ -25,7 +25,7 @@ import { SessionStorageService } from '../../../shared/services/session-storage.
import { SoundService } from '../../../shared/services/sound.service';
import { TokenStorageService } from '../../../shared/services/token-storage.service';
import { RecordingService } from '../../recordings/services/recording.service';
import { RoomMemberService } from '../../rooms/services/room-member.service';
import { RoomMemberContextService } from '../../room-members/services/room-member-context.service';
import { MeetingContextService } from './meeting-context.service';
import { MeetingWebComponentManagerService } from './meeting-webcomponent-manager.service';
@ -40,7 +40,7 @@ export class MeetingEventHandlerService {
protected meetingContext = inject(MeetingContextService);
protected featureConfService = inject(FeatureConfigurationService);
protected recordingService = inject(RecordingService);
protected roomMemberService = inject(RoomMemberService);
protected roomMemberService = inject(RoomMemberContextService);
protected sessionStorageService = inject(SessionStorageService);
protected tokenStorageService = inject(TokenStorageService);
protected wcManagerService = inject(MeetingWebComponentManagerService);

View File

@ -8,7 +8,7 @@ import { AppDataService } from '../../../shared/services/app-data.service';
import { NavigationService } from '../../../shared/services/navigation.service';
import { AuthService } from '../../auth/services/auth.service';
import { RecordingService } from '../../recordings/services/recording.service';
import { RoomMemberService } from '../../rooms/services/room-member.service';
import { RoomMemberContextService } from '../../room-members/services/room-member-context.service';
import { RoomService } from '../../rooms/services/room.service';
import { LobbyState } from '../models/lobby.model';
import { MeetingContextService } from './meeting-context.service';
@ -46,7 +46,7 @@ export class MeetingLobbyService {
protected meetingService: MeetingService = inject(MeetingService);
protected recordingService: RecordingService = inject(RecordingService);
protected authService: AuthService = inject(AuthService);
protected roomMemberService: RoomMemberService = inject(RoomMemberService);
protected roomMemberService: RoomMemberContextService = inject(RoomMemberContextService);
protected navigationService: NavigationService = inject(NavigationService);
protected appDataService: AppDataService = inject(AppDataService);
protected wcManagerService: MeetingWebComponentManagerService = inject(MeetingWebComponentManagerService);
@ -287,7 +287,7 @@ export class MeetingLobbyService {
*/
protected async checkForRecordings(): Promise<void> {
try {
const canRetrieveRecordings = this.roomMemberService.canRetrieveRecordings();
const canRetrieveRecordings = this.roomMemberService.hasPermission('canRetrieveRecordings');
if (!canRetrieveRecordings) {
this._state.update((state) => ({ ...state, showRecordingCard: false }));

View File

@ -7,7 +7,7 @@ import {
} from '@openvidu-meet/typings';
import { LoggerService, OpenViduService } from 'openvidu-components-angular';
import { AppDataService } from '../../../shared/services/app-data.service';
import { RoomMemberService } from '../../rooms/services/room-member.service';
import { RoomMemberContextService } from '../../room-members/services/room-member-context.service';
import { MeetingContextService } from './meeting-context.service';
import { MeetingService } from './meeting.service';
@ -26,7 +26,7 @@ export class MeetingWebComponentManagerService {
protected log;
protected readonly meetingContextService = inject(MeetingContextService);
protected readonly roomMemberService = inject(RoomMemberService);
protected readonly roomMemberService = inject(RoomMemberContextService);
protected readonly openviduService = inject(OpenViduService);
protected readonly meetingService = inject(MeetingService);
protected readonly loggerService = inject(LoggerService);
@ -121,9 +121,11 @@ export class MeetingWebComponentManagerService {
console.debug('Message received from parent:', event.data);
switch (command) {
case WebComponentCommand.END_MEETING:
// Only moderators can end the meeting
if (!this.roomMemberService.isModerator()) {
this.log.w('End meeting command received but participant is not a moderator');
// Only participants with canEndMeeting permission can end the meeting
if (!this.roomMemberService.hasPermission('canEndMeeting')) {
this.log.w(
'End meeting command received but participant does not have permissions to end the meeting'
);
return;
}
@ -141,9 +143,11 @@ export class MeetingWebComponentManagerService {
await this.openviduService.disconnectRoom();
break;
case WebComponentCommand.KICK_PARTICIPANT:
// Only moderators can kick participants
if (!this.roomMemberService.isModerator()) {
this.log.w('Kick participant command received but participant is not a moderator');
// Only participants with canKickParticipants permission can kick participants
if (!this.roomMemberService.hasPermission('canKickParticipants')) {
this.log.w(
'Kick participant command received but participant does not have permissions to kick participants'
);
return;
}

View File

@ -9,7 +9,7 @@ import { ILogger, LoggerService } from 'openvidu-components-angular';
import { NavigationService } from '../../../../shared/services/navigation.service';
import { NotificationService } from '../../../../shared/services/notification.service';
import { MeetingContextService } from '../../../meeting/services';
import { RoomMemberService } from '../../../rooms/services/room-member.service';
import { RoomMemberContextService } from '../../../room-members/services/room-member-context.service';
import { RecordingListsComponent } from '../../components/recording-lists/recording-lists.component';
import { RecordingTableAction, RecordingTableFilter } from '../../models/recording-list.model';
import { RecordingService } from '../../services/recording.service';
@ -46,7 +46,7 @@ export class RoomRecordingsComponent implements OnInit {
protected readonly loggerService = inject(LoggerService);
protected readonly recordingService = inject(RecordingService);
protected readonly roomMemberService = inject(RoomMemberService);
protected readonly roomMemberService = inject(RoomMemberContextService);
protected readonly notificationService = inject(NotificationService);
protected readonly navigationService = inject(NavigationService);
protected readonly meetingContextService = inject(MeetingContextService);
@ -58,7 +58,7 @@ export class RoomRecordingsComponent implements OnInit {
async ngOnInit() {
this.roomId = this.route.snapshot.paramMap.get('room-id')!;
this.canDeleteRecordings = this.roomMemberService.canDeleteRecordings();
this.canDeleteRecordings = this.roomMemberService.hasPermission('canDeleteRecordings');
// Load recordings
const delayLoader = setTimeout(() => {

View File

@ -0,0 +1,3 @@
export * from './interceptor-handlers';
export * from './providers';
export * from './services';

View File

@ -8,7 +8,7 @@ import {
} from '../../../shared/services/http-error-notifier.service';
import { TokenStorageService } from '../../../shared/services/token-storage.service';
import { MeetingContextService } from '../../meeting/services/meeting-context.service';
import { RoomMemberService } from '../services/room-member.service';
import { RoomMemberContextService } from '../services/room-member-context.service';
/**
* Handler for room member token-related HTTP errors.
@ -19,7 +19,7 @@ import { RoomMemberService } from '../services/room-member.service';
providedIn: 'root'
})
export class RoomMemberInterceptorErrorHandlerService implements HttpErrorHandler {
private readonly roomMemberService = inject(RoomMemberService);
private readonly roomMemberService = inject(RoomMemberContextService);
private readonly meetingContextService = inject(MeetingContextService);
private readonly tokenStorageService = inject(TokenStorageService);
private readonly httpErrorNotifier = inject(HttpErrorNotifierService);

View File

@ -1,6 +1,6 @@
import { Provider } from '@angular/core';
import { ROOM_MEMBER_ADAPTER } from '../../../shared/adapters';
import { RoomMemberService } from '../services/room-member.service';
import { RoomMemberContextService } from '../../room-members/services/room-member-context.service';
/**
* Provides the RoomMemberAdapter using the existing RoomMemberService.
@ -8,5 +8,5 @@ import { RoomMemberService } from '../services/room-member.service';
*/
export const ROOM_MEMBER_ADAPTER_PROVIDER: Provider = {
provide: ROOM_MEMBER_ADAPTER,
useExisting: RoomMemberService
useExisting: RoomMemberContextService
};

View File

@ -0,0 +1,2 @@
export * from './room-member-context.service';
export * from './room-member.service';

View File

@ -7,15 +7,14 @@ import {
} from '@openvidu-meet/typings';
import { E2eeService, LoggerService } from 'openvidu-components-angular';
import { FeatureConfigurationService } from '../../../shared/services/feature-configuration.service';
import { HttpService } from '../../../shared/services/http.service';
import { TokenStorageService } from '../../../shared/services/token-storage.service';
import { getValidDecodedToken } from '../../../shared/utils/token.utils';
import { decodeToken } from '../../../shared/utils/token.utils';
import { RoomMemberService } from './room-member.service';
@Injectable({
providedIn: 'root'
})
export class RoomMemberService {
protected readonly ROOM_MEMBERS_API = `${HttpService.INTERNAL_API_PATH_PREFIX}/rooms`;
export class RoomMemberContextService {
protected readonly PARTICIPANT_NAME_KEY = 'ovMeet-participantName';
protected participantName?: string;
@ -27,39 +26,65 @@ export class RoomMemberService {
constructor(
protected loggerService: LoggerService,
protected httpService: HttpService,
protected roomMemberService: RoomMemberService,
protected featureConfService: FeatureConfigurationService,
protected tokenStorageService: TokenStorageService,
protected e2eeService: E2eeService
) {
this.log = this.loggerService.get('OpenVidu Meet - ParticipantTokenService');
}
protected getRoomMemberApiPath(roomId: string): string {
return `${this.ROOM_MEMBERS_API}/${roomId}/members`;
this.log = this.loggerService.get('OpenVidu Meet - RoomMemberContextService');
}
/**
* Sets the participant's display name and stores it in localStorage.
*
* @param participantName - The display name of the participant
*/
setParticipantName(participantName: string): void {
this.participantName = participantName;
localStorage.setItem(this.PARTICIPANT_NAME_KEY, participantName);
}
/**
* Retrieves the participant's display name from memory or localStorage.
*
* @returns The display name of the participant, or undefined if not set
*/
getParticipantName(): string | undefined {
return this.participantName || localStorage.getItem(this.PARTICIPANT_NAME_KEY) || undefined;
}
/**
* Retrieves the participant's identity.
*
* @returns The identity of the participant, or undefined if not set
*/
getParticipantIdentity(): string | undefined {
return this.participantIdentity;
}
/**
* Clears the participant's identity.
*/
clearParticipantIdentity(): void {
this.participantIdentity = undefined;
}
/**
* Generates a room member token and extracts role/permissions
* Checks if the current room member has a specific permission.
*
* @param permission - The permission to check
* @returns True if the member has the permission, false otherwise
*/
hasPermission(permission: keyof MeetRoomMemberPermissions): boolean {
return this.permissions?.[permission] ?? false;
}
/**
* Generates a room member token and updates the context with role and permissions.
*
* @param roomId - The unique identifier of the room
* @param tokenOptions - The options for the token generation
* @param e2eeKey - Optional E2EE encryption key
* @return A promise that resolves to the room member token
*/
async generateToken(roomId: string, tokenOptions: MeetRoomMemberTokenOptions, e2eeKey?: string): Promise<string> {
@ -70,23 +95,21 @@ export class RoomMemberService {
tokenOptions.participantName = encryptedName;
}
const path = `${this.getRoomMemberApiPath(roomId)}/token`;
const { token } = await this.httpService.postRequest<{ token: string }>(path, tokenOptions);
const { token } = await this.roomMemberService.generateRoomMemberToken(roomId, tokenOptions);
this.tokenStorageService.setRoomMemberToken(token);
await this.updateRoomMemberTokenInfo(token);
await this.updateContextFromToken(token);
return token;
}
/**
* Updates the current room member token information, including role and permissions.
* Updates the room member context (role and permissions) based on the provided token.
*
* @param token - The JWT token to set.
* @param token - The room member token
* @throws Error if the token is invalid or expired.
*/
protected async updateRoomMemberTokenInfo(token: string): Promise<void> {
protected async updateContextFromToken(token: string): Promise<void> {
try {
const decodedToken = getValidDecodedToken(token);
const decodedToken = decodeToken(token);
const metadata = decodedToken.metadata as MeetRoomMemberTokenMetadata;
if (decodedToken.sub && decodedToken.name) {
@ -102,24 +125,8 @@ export class RoomMemberService {
this.featureConfService.setRoomMemberRole(this.role);
this.featureConfService.setRoomMemberPermissions(this.permissions);
} catch (error) {
this.log.e('Error updating room member token info', error);
throw new Error('Error updating room member token info');
this.log.e('Error decoding room member token:', error);
throw new Error('Invalid room member token');
}
}
isModerator(): boolean {
return this.role === MeetRoomMemberRole.MODERATOR;
}
getRoomMemberPermissions(): MeetRoomMemberPermissions | undefined {
return this.permissions;
}
canRetrieveRecordings(): boolean {
return this.permissions?.canRetrieveRecordings ?? false;
}
canDeleteRecordings(): boolean {
return this.permissions?.canDeleteRecordings ?? false;
}
}

View File

@ -0,0 +1,155 @@
import { Injectable } from '@angular/core';
import {
MeetRoomMember,
MeetRoomMemberFilters,
MeetRoomMemberOptions,
MeetRoomMemberTokenOptions
} from '@openvidu-meet/typings';
import { HttpService } from '../../../shared/services/http.service';
@Injectable({
providedIn: 'root'
})
export class RoomMemberService {
protected readonly ROOM_MEMBERS_API = `${HttpService.INTERNAL_API_PATH_PREFIX}/rooms`;
constructor(protected httpService: HttpService) {}
/**
* Constructs the API path for room member operations based on the provided room ID.
*
* @param roomId - The unique identifier of the room
* @returns The API path for room member operations
*/
protected getRoomMemberApiPath(roomId: string): string {
return `${this.ROOM_MEMBERS_API}/${roomId}/members`;
}
/**
* Creates a new room member with the specified options.
*
* @param roomId - The unique identifier of the room
* @param options - The options for creating the room member
* @returns A promise that resolves to the created MeetRoomMember object
*/
async createRoomMember(roomId: string, options: MeetRoomMemberOptions): Promise<MeetRoomMember> {
const path = this.getRoomMemberApiPath(roomId);
return this.httpService.postRequest(path, options);
}
/**
* Lists room members with optional filters.
*
* @param roomId - The unique identifier of the room
* @param filters - Optional filters for pagination and fields
* @returns A promise that resolves to an object containing room members and pagination info
*/
async listRoomMembers(
roomId: string,
filters?: MeetRoomMemberFilters
): Promise<{
members: MeetRoomMember[];
pagination: {
isTruncated: boolean;
nextPageToken?: string;
maxItems: number;
};
}> {
let path = this.getRoomMemberApiPath(roomId);
if (filters) {
const queryParams = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
queryParams.set(key, value.toString());
}
});
const queryString = queryParams.toString();
if (queryString) {
path += `?${queryString}`;
}
}
return this.httpService.getRequest(path);
}
/**
* Gets a specific room member by their ID.
*
* @param roomId - The unique identifier of the room
* @param memberId - The unique identifier of the room member
* @returns A promise that resolves to the MeetRoomMember object
*/
async getRoomMember(roomId: string, memberId: string): Promise<MeetRoomMember> {
const path = `${this.getRoomMemberApiPath(roomId)}/${memberId}`;
return this.httpService.getRequest(path);
}
/**
* Updates a room member's information.
*
* @param roomId - The unique identifier of the room
* @param memberId - The unique identifier of the room member
* @param updates - The updates to apply to the room member
* @returns A promise that resolves to the updated MeetRoomMember object
*/
async updateRoomMember(
roomId: string,
memberId: string,
updates: Partial<MeetRoomMemberOptions>
): Promise<MeetRoomMember> {
const path = `${this.getRoomMemberApiPath(roomId)}/${memberId}`;
return this.httpService.putRequest(path, updates);
}
/**
* Deletes a room member by their ID.
*
* @param roomId - The unique identifier of the room
* @param memberId - The unique identifier of the room member to delete
* @returns A promise that resolves when the room member is deleted
*/
async deleteRoomMember(roomId: string, memberId: string): Promise<void> {
const path = `${this.getRoomMemberApiPath(roomId)}/${memberId}`;
return this.httpService.deleteRequest(path);
}
/**
* Bulk deletes multiple room members by their IDs.
*
* @param roomId - The unique identifier of the room
* @param memberIds - An array of room member IDs to delete
* @returns A promise that resolves when the room members are deleted
*/
async bulkDeleteRoomMembers(
roomId: string,
memberIds: string[]
): Promise<{
message: string;
deleted: string[];
}> {
if (memberIds.length === 0) {
throw new Error('No room member IDs provided for bulk deletion');
}
const path = `${this.getRoomMemberApiPath(roomId)}?memberIds=${memberIds.join(',')}`;
return this.httpService.deleteRequest(path);
}
/**
* Generates a room member token for accessing the room and its resources.
*
* @param roomId - The unique identifier of the room
* @param tokenOptions - The options for the token generation
* @returns A promise that resolves to an object containing the generated token
*/
async generateRoomMemberToken(
roomId: string,
tokenOptions: MeetRoomMemberTokenOptions
): Promise<{ token: string }> {
const path = `${this.getRoomMemberApiPath(roomId)}/token`;
return this.httpService.postRequest(path, tokenOptions);
}
}

View File

@ -1,9 +1,10 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
import { NavigationErrorReason } from '../../../shared/models/navigation.model';
import { NavigationService } from '../../../shared/services';
import { MeetingContextService } from '../../meeting/services';
import { RoomMemberService } from '../services';
import { RoomMemberContextService } from '../../room-members/services/room-member-context.service';
import { NavigationService } from '../../../shared/services/navigation.service';
import { MeetingContextService } from '../../meeting/services/meeting-context.service';
/**
* Guard to validate access to a room by generating a room member token.
@ -33,7 +34,7 @@ export const validateRoomRecordingsAccessGuard: CanActivateFn = async (
* @returns True if access is granted, or UrlTree for redirection
*/
const validateRoomAccessInternal = async (pageUrl: string, validateRecordingPermissions = false) => {
const roomMemberService = inject(RoomMemberService);
const roomMemberService = inject(RoomMemberContextService);
const navigationService = inject(NavigationService);
const meetingContextService = inject(MeetingContextService);
@ -56,7 +57,7 @@ const validateRoomAccessInternal = async (pageUrl: string, validateRecordingPerm
// Perform recording validation if requested
if (validateRecordingPermissions) {
if (!roomMemberService.canRetrieveRecordings()) {
if (!roomMemberService.hasPermission('canRetrieveRecordings')) {
// If the user does not have permission to retrieve recordings, redirect to the error page
return navigationService.redirectToErrorPage(NavigationErrorReason.UNAUTHORIZED_RECORDING_ACCESS);
}

View File

@ -1,7 +1,5 @@
export * from './components';
export * from './guards';
export * from './interceptor-handlers';
export * from './models';
export * from './pages';
export * from './providers';
export * from './services';

View File

@ -1,3 +1,2 @@
export * from './room-member.service';
export * from './room.service';
export * from './wizard-state.service';

View File

@ -6,4 +6,3 @@ export * from './models';
export * from './routes/base-routes';
export * from './services';
export * from './utils';

View File

@ -1,6 +1,6 @@
import { jwtDecode } from 'jwt-decode';
export const getValidDecodedToken = (token: string) => {
export const decodeToken = (token: string) => {
checkIsJWTValid(token);
const decodedToken: any = jwtDecode(token);
decodedToken.metadata = JSON.parse(decodedToken.metadata);

View File

@ -10,6 +10,6 @@ export * from './lib/domains/auth';
export * from './lib/domains/console';
export * from './lib/domains/meeting';
export * from './lib/domains/recordings';
export * from './lib/domains/room-members';
export * from './lib/domains/rooms';
export * from './lib/domains/users';