frontend: rename RoomMemberContextService references and implement RoomMemberContextAdapter for improved context management

This commit is contained in:
juancarmore 2026-02-10 12:47:51 +01:00
parent 7cdfdf20f9
commit e45aa91d90
18 changed files with 92 additions and 67 deletions

View File

@ -33,7 +33,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(RoomMemberContextService);
const roomMemberContextService = inject(RoomMemberContextService);
const navigationService = inject(NavigationService);
const meetingContextService = inject(MeetingContextService);
@ -45,14 +45,14 @@ const validateRoomAccessInternal = async (pageUrl: string, validateRecordingPerm
}
try {
await roomMemberService.generateToken(roomId, {
await roomMemberContextService.generateToken(roomId, {
secret,
joinMeeting: false
});
// Perform recording validation if requested
if (validateRecordingPermissions) {
if (!roomMemberService.hasPermission('canRetrieveRecordings')) {
if (!roomMemberContextService.hasPermission('canRetrieveRecordings')) {
// If the user does not have permission to retrieve recordings, redirect to the error page
return navigationService.redirectToErrorPage(NavigationErrorReason.FORBIDDEN_RECORDING_ACCESS);
}

View File

@ -63,7 +63,7 @@ export class MeetingComponent implements OnInit {
protected isMeetingLeft = signal(false);
protected features: Signal<RoomFeatures>;
protected participantService = inject(RoomMemberContextService);
protected roomMemberContextService = inject(RoomMemberContextService);
protected roomFeatureService = inject(RoomFeatureService);
protected ovThemeService = inject(OpenViduThemeService);
protected configService = inject(GlobalConfigService);

View File

@ -40,7 +40,7 @@ export class MeetingEventHandlerService {
protected meetingContext = inject(MeetingContextService);
protected roomFeatureService = inject(RoomFeatureService);
protected recordingService = inject(RecordingService);
protected roomMemberService = inject(RoomMemberContextService);
protected roomMemberContextService = inject(RoomMemberContextService);
protected sessionStorageService = inject(SessionStorageService);
protected tokenStorageService = inject(TokenStorageService);
protected wcManagerService = inject(MeetingWebComponentManagerService);
@ -158,8 +158,7 @@ export class MeetingEventHandlerService {
this.wcManagerService.sendMessageToParent(message);
// Clear participant identity and token
this.roomMemberService.clearParticipantIdentity();
this.tokenStorageService.clearRoomMemberToken();
this.roomMemberContextService.clearContext();
// Clean up room secret and e2ee key (if any), except on browser unload)
if (event.reason !== ParticipantLeftReason.BROWSER_UNLOAD) {
@ -257,7 +256,7 @@ export class MeetingEventHandlerService {
const { participantIdentity, newRole, secret } = event;
const roomId = this.meetingContext.roomId();
const local = this.meetingContext.localParticipant();
const participantName = this.roomMemberService.getParticipantName();
const participantName = this.roomMemberContextService.getParticipantName();
// Check if the role update is for the local participant
if (local && participantIdentity === local.identity) {
@ -269,7 +268,7 @@ export class MeetingEventHandlerService {
try {
// Refresh participant token with new role
await this.roomMemberService.generateToken(roomId, {
await this.roomMemberContextService.generateToken(roomId, {
secret,
joinMeeting: true,
participantName,

View File

@ -46,7 +46,7 @@ export class MeetingLobbyService {
protected meetingService: MeetingService = inject(MeetingService);
protected recordingService: RecordingService = inject(RecordingService);
protected authService: AuthService = inject(AuthService);
protected roomMemberService: RoomMemberContextService = inject(RoomMemberContextService);
protected roomMemberContextService: RoomMemberContextService = inject(RoomMemberContextService);
protected navigationService: NavigationService = inject(NavigationService);
protected appCtxService: AppContextService = inject(AppContextService);
protected wcManagerService: MeetingWebComponentManagerService = inject(MeetingWebComponentManagerService);
@ -290,7 +290,7 @@ export class MeetingLobbyService {
*/
protected async checkForRecordings(): Promise<void> {
try {
const canRetrieveRecordings = this.roomMemberService.hasPermission('canRetrieveRecordings');
const canRetrieveRecordings = this.roomMemberContextService.hasPermission('canRetrieveRecordings');
if (!canRetrieveRecordings) {
this._state.update((state) => ({ ...state, showRecordingCard: false }));
@ -332,7 +332,7 @@ export class MeetingLobbyService {
*/
protected async initializeParticipantName(): Promise<void> {
// Apply participant name from RoomMemberService if set, otherwise use authenticated username
const currentParticipantName = this.roomMemberService.getParticipantName();
const currentParticipantName = this.roomMemberContextService.getParticipantName();
const username = await this.authService.getUserName();
const participantName = currentParticipantName || username;
@ -350,7 +350,7 @@ export class MeetingLobbyService {
try {
const roomId = this._state().roomId;
const roomSecret = this.meetingContextService.roomSecret();
const roomMemberToken = await this.roomMemberService.generateToken(
const roomMemberToken = await this.roomMemberContextService.generateToken(
roomId!,
{
secret: roomSecret,
@ -359,7 +359,7 @@ export class MeetingLobbyService {
},
this.e2eeKeyValue()
);
const updatedName = this.roomMemberService.getParticipantName()!;
const updatedName = this.roomMemberContextService.getParticipantName()!;
this.setParticipantName(updatedName);
this._state.update((state) => ({ ...state, roomMemberToken }));
} catch (error: any) {

View File

@ -26,7 +26,7 @@ export class MeetingWebComponentManagerService {
protected log;
protected readonly meetingContextService = inject(MeetingContextService);
protected readonly roomMemberService = inject(RoomMemberContextService);
protected readonly roomMemberContextService = inject(RoomMemberContextService);
protected readonly openviduService = inject(OpenViduService);
protected readonly meetingService = inject(MeetingService);
protected readonly loggerService = inject(LoggerService);
@ -122,7 +122,7 @@ export class MeetingWebComponentManagerService {
switch (command) {
case WebComponentCommand.END_MEETING:
// Only participants with canEndMeeting permission can end the meeting
if (!this.roomMemberService.hasPermission('canEndMeeting')) {
if (!this.roomMemberContextService.hasPermission('canEndMeeting')) {
this.log.w(
'End meeting command received but participant does not have permissions to end the meeting'
);
@ -144,7 +144,7 @@ export class MeetingWebComponentManagerService {
break;
case WebComponentCommand.KICK_PARTICIPANT:
// Only participants with canKickParticipants permission can kick participants
if (!this.roomMemberService.hasPermission('canKickParticipants')) {
if (!this.roomMemberContextService.hasPermission('canKickParticipants')) {
this.log.w(
'Kick participant command received but participant does not have permissions to kick participants'
);

View File

@ -46,7 +46,7 @@ export class RoomRecordingsComponent implements OnInit {
protected readonly loggerService = inject(LoggerService);
protected readonly recordingService = inject(RecordingService);
protected readonly roomMemberService = inject(RoomMemberContextService);
protected readonly roomMemberContextService = 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.hasPermission('canDeleteRecordings');
this.canDeleteRecordings = this.roomMemberContextService.hasPermission('canDeleteRecordings');
// Load recordings
const delayLoader = setTimeout(() => {

View File

@ -4,6 +4,7 @@ import { MeetRecordingFilters, MeetRecordingInfo } from '@openvidu-meet/typings'
import { LoggerService } from 'openvidu-components-angular';
import { HttpService } from '../../../shared/services/http.service';
import { TokenStorageService } from '../../../shared/services/token-storage.service';
import { RoomMemberContextService } from '../../room-members/services/room-member-context.service';
import { RecordingShareDialogComponent } from '../components/recording-share-dialog/recording-share-dialog.component';
@Injectable({
@ -19,6 +20,7 @@ export class RecordingService {
protected loggerService: LoggerService,
private httpService: HttpService,
protected tokenStorageService: TokenStorageService,
protected roomMemberContextService: RoomMemberContextService,
protected dialog: MatDialog
) {
this.log = this.loggerService.get('OpenVidu Meet - RecordingManagerService');
@ -130,7 +132,7 @@ export class RecordingService {
params.append('accessToken', accessToken);
}
const roomMemberToken = this.tokenStorageService.getRoomMemberToken();
const roomMemberToken = this.roomMemberContextService.getRoomMemberToken();
if (roomMemberToken) {
params.append('roomMemberToken', roomMemberToken);
}
@ -226,7 +228,7 @@ export class RecordingService {
params.append('accessToken', accessToken);
}
const roomMemberToken = this.tokenStorageService.getRoomMemberToken();
const roomMemberToken = this.roomMemberContextService.getRoomMemberToken();
if (roomMemberToken) {
params.append('roomMemberToken', roomMemberToken);
}

View File

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

View File

@ -6,7 +6,6 @@ import {
HttpErrorHandler,
HttpErrorNotifierService
} from '../../../shared/services/http-error-notifier.service';
import { TokenStorageService } from '../../../shared/services/token-storage.service';
import { MeetingContextService } from '../../meeting/services/meeting-context.service';
import { RoomMemberContextService } from '../services/room-member-context.service';
@ -19,9 +18,8 @@ import { RoomMemberContextService } from '../services/room-member-context.servic
providedIn: 'root'
})
export class RoomMemberInterceptorErrorHandlerService implements HttpErrorHandler {
private readonly roomMemberService = inject(RoomMemberContextService);
private readonly roomMemberContextService = inject(RoomMemberContextService);
private readonly meetingContextService = inject(MeetingContextService);
private readonly tokenStorageService = inject(TokenStorageService);
private readonly httpErrorNotifier = inject(HttpErrorNotifierService);
/**
@ -77,12 +75,12 @@ export class RoomMemberInterceptorErrorHandlerService implements HttpErrorHandle
return throwError(() => originalError);
}
const participantName = this.roomMemberService.getParticipantName();
const participantIdentity = this.roomMemberService.getParticipantIdentity();
const participantName = this.roomMemberContextService.getParticipantName();
const participantIdentity = this.roomMemberContextService.getParticipantIdentity();
const joinMeeting = !!participantIdentity; // Grant join permission if identity is set
return from(
this.roomMemberService.generateToken(roomId, {
this.roomMemberContextService.generateToken(roomId, {
secret,
joinMeeting,
participantName,
@ -92,7 +90,7 @@ export class RoomMemberInterceptorErrorHandlerService implements HttpErrorHandle
switchMap(() => {
console.log('Room member token refreshed');
// Update the request with the new token
const newToken = this.tokenStorageService.getRoomMemberToken();
const newToken = this.roomMemberContextService.getRoomMemberToken();
const updatedRequest = newToken
? originalRequest.clone({
setHeaders: {

View File

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

View File

@ -0,0 +1,12 @@
import { Provider } from '@angular/core';
import { ROOM_MEMBER_CONTEXT_ADAPTER } from '../../../shared/adapters/room-member-context.adapter';
import { RoomMemberContextService } from '../services/room-member-context.service';
/**
* Provides the RoomMemberContextAdapter using the existing RoomMemberContextService.
* This allows shared guards to use the adapter interface without depending on domain services.
*/
export const ROOM_MEMBER_CONTEXT_ADAPTER_PROVIDER: Provider = {
provide: ROOM_MEMBER_CONTEXT_ADAPTER,
useExisting: RoomMemberContextService
};

View File

@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import {
MeetRoomMemberPermissions,
MeetRoomMemberRole,
MeetRoomMemberTokenMetadata,
MeetRoomMemberTokenOptions
} from '@openvidu-meet/typings';
@ -17,9 +16,9 @@ import { RoomMemberService } from './room-member.service';
export class RoomMemberContextService {
protected readonly PARTICIPANT_NAME_KEY = 'ovMeet-participantName';
protected roomMemberToken?: string;
protected participantName?: string;
protected participantIdentity?: string;
protected role: MeetRoomMemberRole = MeetRoomMemberRole.SPEAKER;
protected permissions?: MeetRoomMemberPermissions;
protected log;
@ -34,6 +33,15 @@ export class RoomMemberContextService {
this.log = this.loggerService.get('OpenVidu Meet - RoomMemberContextService');
}
/**
* Retrieves the current room member token.
*
* @returns The room member token, or undefined if not set
*/
getRoomMemberToken(): string | undefined {
return this.roomMemberToken;
}
/**
* Sets the participant's display name and stores it in localStorage.
*
@ -62,13 +70,6 @@ export class RoomMemberContextService {
return this.participantIdentity;
}
/**
* Clears the participant's identity.
*/
clearParticipantIdentity(): void {
this.participantIdentity = undefined;
}
/**
* Checks if the current room member has a specific permission.
*
@ -96,13 +97,13 @@ export class RoomMemberContextService {
}
const { token } = await this.roomMemberService.generateRoomMemberToken(roomId, tokenOptions);
this.tokenStorageService.setRoomMemberToken(token);
this.roomMemberToken = token;
await this.updateContextFromToken(token);
return token;
}
/**
* Updates the room member context (role and permissions) based on the provided token.
* Updates the room member context based on the provided token.
*
* @param token - The room member token
* @throws Error if the token is invalid or expired.
@ -118,15 +119,24 @@ export class RoomMemberContextService {
this.participantIdentity = decodedToken.sub;
}
this.role = metadata.baseRole;
this.permissions = metadata.effectivePermissions;
// Update feature configuration
this.roomFeatureService.setRoomMemberRole(this.role);
this.roomFeatureService.setRoomMemberRole(metadata.baseRole);
this.roomFeatureService.setRoomMemberPermissions(this.permissions);
} catch (error) {
this.log.e('Error decoding room member token:', error);
throw new Error('Invalid room member token');
}
}
/**
* Clears the room member context, including token, participant info, role, and permissions.
*/
clearContext(): void {
this.roomMemberToken = undefined;
this.participantName = undefined;
this.participantIdentity = undefined;
this.permissions = undefined;
}
}

View File

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

View File

@ -0,0 +1,17 @@
import { InjectionToken } from '@angular/core';
/**
* Adapter interface for room member context, providing necessary methods to access room member information.
* This allows shared services to interact with room member context without directly depending on domain services.
*/
export interface RoomMemberContextAdapter {
/**
* Retrieves the current room member token.
*/
getRoomMemberToken(): string;
}
/**
* Injection token for the RoomMemberContextAdapter
*/
export const ROOM_MEMBER_CONTEXT_ADAPTER = new InjectionToken<RoomMemberContextAdapter>('ROOM_MEMBER_CONTEXT_ADAPTER');

View File

@ -1,3 +1,4 @@
export * from './adapters';
export * from './components';
export * from './guards';
export * from './interceptors';

View File

@ -2,6 +2,7 @@ import { HttpErrorResponse, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
import { ROOM_MEMBER_CONTEXT_ADAPTER, RoomMemberContextAdapter } from '../adapters';
import { HttpErrorNotifierService, TokenStorageService } from '../services';
/**
@ -11,7 +12,8 @@ import { HttpErrorNotifierService, TokenStorageService } from '../services';
*/
const addAuthHeadersIfNeeded = (
req: HttpRequest<unknown>,
tokenStorageService: TokenStorageService
tokenStorageService: TokenStorageService,
roomMemberContextService: RoomMemberContextAdapter
): HttpRequest<unknown> => {
const headers: { [key: string]: string } = {};
@ -22,7 +24,7 @@ const addAuthHeadersIfNeeded = (
}
// Add room member token header if available
const roomMemberToken = tokenStorageService.getRoomMemberToken();
const roomMemberToken = roomMemberContextService.getRoomMemberToken();
if (roomMemberToken) {
headers['x-room-member-token'] = `Bearer ${roomMemberToken}`;
}
@ -42,12 +44,13 @@ const addAuthHeadersIfNeeded = (
export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => {
const router = inject(Router);
const tokenStorageService = inject(TokenStorageService);
const roomMemberContextService = inject(ROOM_MEMBER_CONTEXT_ADAPTER);
const httpErrorNotifier = inject(HttpErrorNotifierService);
const pageUrl = router.currentNavigation()?.finalUrl?.toString() || router.url;
// Add all authorization headers if tokens exist
req = addAuthHeadersIfNeeded(req, tokenStorageService);
req = addAuthHeadersIfNeeded(req, tokenStorageService, roomMemberContextService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {

View File

@ -1,8 +1,7 @@
import { Injectable } from '@angular/core';
/**
* Service to manage JWT token storage when using header-based authentication.
* Tokens are stored in localStorage/sessionStorage when authTransportMode is 'header'.
* Service to manage JWT token storage for authentication
*/
@Injectable({
providedIn: 'root'
@ -10,9 +9,6 @@ import { Injectable } from '@angular/core';
export class TokenStorageService {
private readonly ACCESS_TOKEN_KEY = 'ovMeet-accessToken';
private readonly REFRESH_TOKEN_KEY = 'ovMeet-refreshToken';
private readonly ROOM_MEMBER_TOKEN_KEY = 'ovMeet-roomMemberToken';
// ACCESS AND REFRESH TOKEN METHODS
// Saves the access token to localStorage
setAccessToken(token: string): void {
@ -39,22 +35,4 @@ export class TokenStorageService {
localStorage.removeItem(this.ACCESS_TOKEN_KEY);
localStorage.removeItem(this.REFRESH_TOKEN_KEY);
}
// ROOM MEMBER TOKEN METHODS
// Uses sessionStorage instead of localStorage to ensure token is not shared across browser tabs
// Saves the room member token to sessionStorage
setRoomMemberToken(token: string): void {
sessionStorage.setItem(this.ROOM_MEMBER_TOKEN_KEY, token);
}
// Retrieves the room member token from sessionStorage
getRoomMemberToken(): string | null {
return sessionStorage.getItem(this.ROOM_MEMBER_TOKEN_KEY);
}
// Removes the room member token from sessionStorage
clearRoomMemberToken(): void {
sessionStorage.removeItem(this.ROOM_MEMBER_TOKEN_KEY);
}
}

View File

@ -16,6 +16,7 @@ import {
CustomParticipantModel,
httpInterceptor,
MeetingLayoutService,
ROOM_MEMBER_CONTEXT_ADAPTER_PROVIDER,
RoomMemberInterceptorErrorHandlerService,
ThemeService
} from '@openvidu-meet/shared-components';
@ -38,6 +39,7 @@ export const appConfig: ApplicationConfig = {
provideAppInitializer(() => inject(RoomMemberInterceptorErrorHandlerService).init()),
importProvidersFrom(OpenViduComponentsModule.forRoot(ovComponentsconfig)),
{ provide: LayoutService, useClass: MeetingLayoutService },
ROOM_MEMBER_CONTEXT_ADAPTER_PROVIDER,
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(ceRoutes),
provideAnimationsAsync(),