frontend: enhance participant token management and update related services

- Renamed `setParticipantToken` to `setParticipantTokenAndUpdateContext` in ContextService to clarify its functionality.
- Introduced ParticipantTokenService to encapsulate token generation logic and manage role/permissions extraction.
- Updated VideoRoomComponent to utilize the new ParticipantTokenService for generating participant tokens.
- Refactored access room method to improve form validation and participant name initialization.
- Added unit tests for ParticipantTokenService to ensure proper functionality.
- Updated sidenav model comments for clarity.
This commit is contained in:
Carlos Santos 2025-06-09 13:00:36 +02:00
parent 1cd58c19b9
commit b2f1e2194a
9 changed files with 118 additions and 39 deletions

View File

@ -53,7 +53,7 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, ne
return from(httpService.refreshParticipantToken({ roomId, participantName, secret })).pipe( return from(httpService.refreshParticipantToken({ roomId, participantName, secret })).pipe(
switchMap((data) => { switchMap((data) => {
console.log('Participant token refreshed'); console.log('Participant token refreshed');
contextService.setParticipantToken(data.token); contextService.setParticipantTokenAndUpdateContext(data.token);
return next(req); return next(req);
}), }),
catchError((error: HttpErrorResponse) => { catchError((error: HttpErrorResponse) => {

View File

@ -0,0 +1,7 @@
import { OpenViduMeetPermissions, ParticipantRole } from 'shared-meet-components';
export interface TokenGenerationResult {
token: string; // The generated participant token
role: ParticipantRole; // Role of the participant (e.g., 'moderator', 'publisher')
permissions: OpenViduMeetPermissions; // List of permissions granted to the participant
}

View File

@ -1,6 +1,6 @@
export interface ConsoleNavLink { export interface ConsoleNavLink {
label: string; // Nombre del enlace label: string; // Link name
icon?: string; // Icono opcional icon?: string; // Optional icon
route?: string; // Ruta para la navegación (opcional) route?: string; // Route for navigation (optional)
clickHandler?: () => void; // Función para manejar clics (opcional) clickHandler?: () => void; // Function to handle clicks (optional)
} }

View File

@ -5,7 +5,7 @@
<h2 class="form-title"> <h2 class="form-title">
Access room <strong>{{ roomId }}</strong> Access room <strong>{{ roomId }}</strong>
</h2> </h2>
<form [formGroup]="participantForm" (ngSubmit)="accessRoom()"> <form [formGroup]="participantForm" (ngSubmit)="submitAccessRoom()">
<mat-form-field appearance="outline" class="full-width"> <mat-form-field appearance="outline" class="full-width">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input <input

View File

@ -26,6 +26,7 @@ import {
SessionStorageService, SessionStorageService,
WebComponentManagerService WebComponentManagerService
} from '../../services'; } from '../../services';
import { ParticipantTokenService } from '@lib/services/participant-token/participant-token.service';
@Component({ @Component({
selector: 'app-video-room', selector: 'app-video-room',
@ -83,6 +84,7 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
constructor( constructor(
protected httpService: HttpService, protected httpService: HttpService,
protected participantTokenService: ParticipantTokenService,
protected router: Router, protected router: Router,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected location: Location, protected location: Location,
@ -99,32 +101,28 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
const storageSecret = this.sessionStorageService.getModeratorSecret(this.roomId); const storageSecret = this.sessionStorageService.getModeratorSecret(this.roomId);
this.roomSecret = storageSecret || secret; this.roomSecret = storageSecret || secret;
// Apply participant name from context if set, otherwise use authenticated username await this.initializeParticipantName();
const contextParticipantName = this.ctxService.getParticipantName();
const username = await this.authService.getUsername();
const participantName = contextParticipantName || username;
if (participantName) {
this.participantForm.get('name')?.setValue(participantName);
}
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.wcManagerService.stopCommandsListener(); this.wcManagerService.stopCommandsListener();
} }
async accessRoom() { async submitAccessRoom() {
if (!this.participantForm.valid) { const { valid, value } = this.participantForm;
if (!valid || !value.name?.trim()) {
// If the form is invalid, do not proceed
console.warn('Participant form is invalid. Cannot access room.');
return; return;
} }
this.participantName = this.participantForm.value.name!; this.participantName = value.name.trim();
try { try {
await this.generateParticipantToken(); await this.generateParticipantToken();
await this.replaceUrlQueryParams(); await this.replaceUrlQueryParams();
// await this.loadRoomPreferences(); await this.loadRoomPreferences();
this.applyParticipantPermissions(); this.updateFeatureConfiguration();
this.showRoom = true; this.showRoom = true;
} catch (error) { } catch (error) {
console.error('Error accessing room:', error); console.error('Error accessing room:', error);
@ -136,14 +134,44 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.participantToken = this.ctxService.getParticipantToken(); this.participantToken = this.ctxService.getParticipantToken();
} }
/**
* Initializes the participant name in the form control.
*
* Retrieves the participant name from the context service first, and if not available,
* falls back to the authenticated username. Sets the retrieved name value in the
* participant form's 'name' control if a valid name is found.
*
* @private
* @async
* @returns {Promise<void>} A promise that resolves when the participant name has been initialized
*/
private async initializeParticipantName() {
// Apply participant name from context if set, otherwise use authenticated username
const contextParticipantName = this.ctxService.getParticipantName();
const username = await this.authService.getUsername();
const participantName = contextParticipantName || username;
if (participantName) {
this.participantForm.get('name')?.setValue(participantName);
}
}
/**
* Generates a participant token for joining a video room.
*
* @throws {Error} When participant already exists in the room (status 409)
* @returns {Promise<void>} Promise that resolves when token is generated and set, or rejects on participant conflict
*/
private async generateParticipantToken() { private async generateParticipantToken() {
try { try {
const response = await this.httpService.generateParticipantToken({ const { token, role, permissions } = await this.participantTokenService.generateToken(
roomId: this.roomId, this.roomId,
participantName: this.participantName, this.participantName,
secret: this.roomSecret this.roomSecret
}); );
this.setParticipantToken(response.token); this.participantToken = token;
this.participantRole = role;
this.participantPermissions = permissions;
} catch (error: any) { } catch (error: any) {
console.error('Error generating participant token:', error); console.error('Error generating participant token:', error);
switch (error.status) { switch (error.status) {
@ -166,16 +194,6 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
} }
} }
private setParticipantToken(token: string): void {
try {
this.ctxService.setParticipantToken(token);
this.participantRole = this.ctxService.getParticipantRole();
this.participantPermissions = this.ctxService.getParticipantPermissions();
} catch (error: any) {
console.error('Error setting token in context', error);
}
}
private async replaceUrlQueryParams() { private async replaceUrlQueryParams() {
let secretQueryParam = this.roomSecret; let secretQueryParam = this.roomSecret;

View File

@ -119,15 +119,19 @@ export class ContextService {
} }
/** /**
* Sets the token for the current session. * Sets the participant token in the context and updates feature configuration.
* @param token - A string representing the token. * @param token - The JWT token to set.
* @throws Error if the token is invalid or expired.
*/ */
setParticipantToken(token: string): void { setParticipantTokenAndUpdateContext(token: string): void {
try { try {
const decodedToken = this.getValidDecodedToken(token); const decodedToken = this.getValidDecodedToken(token);
this.context.participantToken = token; this.context.participantToken = token;
this.context.participantPermissions = decodedToken.metadata.permissions; this.context.participantPermissions = decodedToken.metadata.permissions;
this.context.participantRole = decodedToken.metadata.role; this.context.participantRole = decodedToken.metadata.role;
// Update feature configuration based on the new token
this.updateFeatureConfiguration();
} catch (error: any) { } catch (error: any) {
this.log.e('Error setting token in context', error); this.log.e('Error setting token in context', error);
throw new Error('Error setting token', error); throw new Error('Error setting token', error);

View File

@ -6,3 +6,4 @@ export * from './room/room.service';
export * from './notification/notification.service'; export * from './notification/notification.service';
export * from './webcomponent-manager/webcomponent-manager.service'; export * from './webcomponent-manager/webcomponent-manager.service';
export * from './session-storage/session-storage.service'; export * from './session-storage/session-storage.service';
export * from './participant-token/participant-token.service';

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ParticipantTokenService } from './participant-token.service';
describe('ParticipantTokenService', () => {
let service: ParticipantTokenService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ParticipantTokenService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { TokenGenerationResult } from '@lib/models/auth.model';
import { HttpService, ContextService, SessionStorageService } from 'shared-meet-components';
@Injectable({
providedIn: 'root'
})
export class ParticipantTokenService {
constructor(
private httpService: HttpService,
private ctxService: ContextService,
private sessionStorageService: SessionStorageService
) {}
/**
* Generates a participant token and extracts role/permissions
*/
async generateToken(roomId: string, participantName: string, secret: string): Promise<TokenGenerationResult> {
const response = await this.httpService.generateParticipantToken({
roomId,
participantName,
secret
});
this.ctxService.setParticipantTokenAndUpdateContext(response.token);
return {
token: response.token,
role: this.ctxService.getParticipantRole(),
permissions: this.ctxService.getParticipantPermissions()
};
}
}