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(
switchMap((data) => {
console.log('Participant token refreshed');
contextService.setParticipantToken(data.token);
contextService.setParticipantTokenAndUpdateContext(data.token);
return next(req);
}),
catchError((error: HttpErrorResponse) => {
@ -127,7 +127,7 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, ne
// refresh the participant token
return refreshParticipantToken(error);
}
// Expired access token
if (!pageUrl.startsWith('/console/login') && !pageUrl.startsWith('/login')) {
// If the error occurred in a page that is not the login page, refresh the access token

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 {
label: string; // Nombre del enlace
icon?: string; // Icono opcional
route?: string; // Ruta para la navegación (opcional)
clickHandler?: () => void; // Función para manejar clics (opcional)
label: string; // Link name
icon?: string; // Optional icon
route?: string; // Route for navigation (optional)
clickHandler?: () => void; // Function to handle clicks (optional)
}

View File

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

View File

@ -26,6 +26,7 @@ import {
SessionStorageService,
WebComponentManagerService
} from '../../services';
import { ParticipantTokenService } from '@lib/services/participant-token/participant-token.service';
@Component({
selector: 'app-video-room',
@ -83,6 +84,7 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
constructor(
protected httpService: HttpService,
protected participantTokenService: ParticipantTokenService,
protected router: Router,
protected route: ActivatedRoute,
protected location: Location,
@ -99,32 +101,28 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
const storageSecret = this.sessionStorageService.getModeratorSecret(this.roomId);
this.roomSecret = storageSecret || secret;
// 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);
}
await this.initializeParticipantName();
}
ngOnDestroy(): void {
this.wcManagerService.stopCommandsListener();
}
async accessRoom() {
if (!this.participantForm.valid) {
async submitAccessRoom() {
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;
}
this.participantName = this.participantForm.value.name!;
this.participantName = value.name.trim();
try {
await this.generateParticipantToken();
await this.replaceUrlQueryParams();
// await this.loadRoomPreferences();
this.applyParticipantPermissions();
await this.loadRoomPreferences();
this.updateFeatureConfiguration();
this.showRoom = true;
} catch (error) {
console.error('Error accessing room:', error);
@ -136,14 +134,44 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
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() {
try {
const response = await this.httpService.generateParticipantToken({
roomId: this.roomId,
participantName: this.participantName,
secret: this.roomSecret
});
this.setParticipantToken(response.token);
const { token, role, permissions } = await this.participantTokenService.generateToken(
this.roomId,
this.participantName,
this.roomSecret
);
this.participantToken = token;
this.participantRole = role;
this.participantPermissions = permissions;
} catch (error: any) {
console.error('Error generating participant token:', error);
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() {
let secretQueryParam = this.roomSecret;

View File

@ -119,15 +119,19 @@ export class ContextService {
}
/**
* Sets the token for the current session.
* @param token - A string representing the token.
* Sets the participant token in the context and updates feature configuration.
* @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 {
const decodedToken = this.getValidDecodedToken(token);
this.context.participantToken = token;
this.context.participantPermissions = decodedToken.metadata.permissions;
this.context.participantRole = decodedToken.metadata.role;
// Update feature configuration based on the new token
this.updateFeatureConfiguration();
} catch (error: any) {
this.log.e('Error setting token in context', 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 './webcomponent-manager/webcomponent-manager.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()
};
}
}