frontend: Add session storage service and integrate moderator secret handling in room access validation
This commit is contained in:
parent
61f1efd021
commit
b4ac4933bf
@ -3,3 +3,4 @@ export * from './extract-query-params.guard';
|
||||
export * from './validate-room-access.guard';
|
||||
export * from './application-mode.guard';
|
||||
export * from './participant-name.guard';
|
||||
export * from './replace-moderator-secret.guard';
|
||||
|
||||
@ -7,10 +7,11 @@ export const checkParticipantNameGuard: CanActivateFn = async (route, state) =>
|
||||
const router = inject(Router);
|
||||
const contextService = inject(ContextService);
|
||||
const roomName = route.params['room-name'];
|
||||
const hasParticipantName = !!contextService.getParticipantName();
|
||||
|
||||
// Check if participant name exists in the service
|
||||
if (!contextService.getParticipantName()) {
|
||||
// Redirect to a page where the user can input their participant name
|
||||
if (!hasParticipantName) {
|
||||
// Redirect to a page where the participant can input their participant name
|
||||
return router.navigate([`${roomName}/participant-name`], {
|
||||
queryParams: { originUrl: state.url, t: Date.now() },
|
||||
skipLocationChange: true
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { CanActivateFn, NavigationEnd } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import { ContextService, HttpService, SessionStorageService } from '../services';
|
||||
import { filter, take } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Guard that replaces the moderator secret in the URL with the publisher secret.
|
||||
*
|
||||
* This guard checks if the current participant is a moderator. If so, it retrieves the moderator and publisher secrets
|
||||
* for the current room and updates the session storage with the moderator secret. It then replaces the secret in the URL
|
||||
* with the publisher secret.
|
||||
*
|
||||
* @param route - The activated route snapshot.
|
||||
* @param state - The router state snapshot.
|
||||
* @returns A promise that resolves to `true` if the operation is successful, otherwise `false`.
|
||||
*
|
||||
* @throws Will log an error and return `false` if an error occurs during the process.
|
||||
*/
|
||||
export const replaceModeratorSecretGuard: CanActivateFn = async (route, state) => {
|
||||
const httpService = inject(HttpService);
|
||||
const contextService = inject(ContextService);
|
||||
const router = inject(Router);
|
||||
const location: Location = inject(Location);
|
||||
const sessionStorageService = inject(SessionStorageService);
|
||||
|
||||
try {
|
||||
router.events
|
||||
.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(async () => {
|
||||
if (contextService.isModeratorParticipant()) {
|
||||
const roomName = contextService.getRoomName();
|
||||
const { moderatorSecret, publisherSecret } = await getUrlSecret(httpService, roomName);
|
||||
|
||||
sessionStorageService.setModeratorSecret(roomName, moderatorSecret);
|
||||
// Replace secret in URL by the publisher secret
|
||||
const queryParams = { ...route.queryParams, secret: publisherSecret };
|
||||
const urlTree = router.createUrlTree([], { queryParams, queryParamsHandling: 'merge' });
|
||||
const newUrl = router.serializeUrl(urlTree);
|
||||
|
||||
location.replaceState(newUrl);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getUrlSecret = async (
|
||||
httpService: HttpService,
|
||||
roomName: string
|
||||
): Promise<{ moderatorSecret: string; publisherSecret: string }> => {
|
||||
const { moderatorRoomUrl, publisherRoomUrl } = await httpService.getRoom(
|
||||
roomName,
|
||||
'moderatorRoomUrl,publisherRoomUrl'
|
||||
);
|
||||
|
||||
const extractSecret = (urlString: string, type: string): string => {
|
||||
const url = new URL(urlString);
|
||||
const secret = url.searchParams.get('secret');
|
||||
if (!secret) throw new Error(`${type} secret not found`);
|
||||
return secret;
|
||||
};
|
||||
|
||||
const publisherSecret = extractSecret(publisherRoomUrl, 'Publisher');
|
||||
const moderatorSecret = extractSecret(moderatorRoomUrl, 'Moderator');
|
||||
|
||||
return { publisherSecret, moderatorSecret };
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, CanActivateFn } from '@angular/router';
|
||||
import { ContextService, HttpService } from '../services';
|
||||
import { ContextService, HttpService, SessionStorageService } from '../services';
|
||||
|
||||
/**
|
||||
* Guard to validate the access to a room.
|
||||
@ -12,30 +12,38 @@ export const validateRoomAccessGuard: CanActivateFn = async (
|
||||
const httpService = inject(HttpService);
|
||||
const contextService = inject(ContextService);
|
||||
const router = inject(Router);
|
||||
const sessionStorageService = inject(SessionStorageService);
|
||||
|
||||
const { roomName, participantName, secret } = extractParams(route);
|
||||
|
||||
if (contextService.isEmbeddedMode() && !contextService.getParticipantName()) {
|
||||
await redirectToUnauthorized(router, 'invalid-participant');
|
||||
return false;
|
||||
}
|
||||
const storageSecret = sessionStorageService.getModeratorSecret(roomName);
|
||||
|
||||
try {
|
||||
// Generate a participant token
|
||||
const response = await httpService.generateParticipantToken({ roomName, participantName, secret });
|
||||
const response = await httpService.generateParticipantToken({
|
||||
roomName,
|
||||
participantName,
|
||||
secret: storageSecret || secret
|
||||
});
|
||||
contextService.setToken(response.token);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error.status === 409) {
|
||||
// Participant already exists
|
||||
// Redirect to a page where the user can input their participant name
|
||||
await router.navigate([`${roomName}/participant-name`], {
|
||||
queryParams: { originUrl: _state.url, accessError: 'participant-exists', t: Date.now() },
|
||||
skipLocationChange: true
|
||||
});
|
||||
return false;
|
||||
console.error('Error generating participant token:', error);
|
||||
switch (error.status) {
|
||||
case 409:
|
||||
// Participant already exists.
|
||||
// Send a timestamp to force update the query params and show the error message in participant name input form
|
||||
await router.navigate([`${roomName}/participant-name`], {
|
||||
queryParams: { originUrl: _state.url, accessError: 'participant-exists', t: Date.now() },
|
||||
skipLocationChange: true
|
||||
});
|
||||
break;
|
||||
case 406:
|
||||
await redirectToUnauthorized(router, 'unauthorized-participant');
|
||||
break;
|
||||
default:
|
||||
await redirectToUnauthorized(router, 'invalid-room');
|
||||
}
|
||||
return await redirectToUnauthorized(router, 'invalid-room');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -7,7 +7,8 @@ import {
|
||||
validateRoomAccessGuard,
|
||||
applicationModeGuard,
|
||||
extractQueryParamsGuard,
|
||||
checkParticipantNameGuard
|
||||
checkParticipantNameGuard,
|
||||
replaceModeratorSecretGuard
|
||||
} from '../guards';
|
||||
import {
|
||||
AboutComponent,
|
||||
@ -77,11 +78,17 @@ export const baseRoutes: Routes = [
|
||||
{
|
||||
path: ':room-name',
|
||||
component: VideoRoomComponent,
|
||||
canActivate: [applicationModeGuard, extractQueryParamsGuard, checkParticipantNameGuard, validateRoomAccessGuard],
|
||||
canActivate: [
|
||||
applicationModeGuard,
|
||||
extractQueryParamsGuard,
|
||||
checkParticipantNameGuard,
|
||||
validateRoomAccessGuard,
|
||||
replaceModeratorSecretGuard
|
||||
]
|
||||
},
|
||||
{
|
||||
path: ':room-name/participant-name',
|
||||
component: ParticipantNameFormComponent,
|
||||
component: ParticipantNameFormComponent
|
||||
},
|
||||
|
||||
// Redirect all other routes to home
|
||||
|
||||
@ -23,13 +23,20 @@ export class HttpService {
|
||||
return this.deleteRequest(`${this.pathPrefix}/${this.apiVersion}/rooms/${roomName}`);
|
||||
}
|
||||
|
||||
listRooms(): Promise<OpenViduMeetRoom[]> {
|
||||
//TODO: Add 'fields' query param for filtering rooms by fields
|
||||
return this.getRequest(`${this.pathPrefix}/${this.apiVersion}/rooms`);
|
||||
listRooms(fields?: string): Promise<OpenViduMeetRoom[]> {
|
||||
let path = `${this.pathPrefix}/${this.apiVersion}/rooms/`;
|
||||
if (fields) {
|
||||
path += `?fields=${encodeURIComponent(fields)}`;
|
||||
}
|
||||
return this.getRequest(path);
|
||||
}
|
||||
|
||||
getRoom(roomName: string): Promise<OpenViduMeetRoom> {
|
||||
return this.getRequest(`${this.pathPrefix}/${this.apiVersion}/rooms/${roomName}`);
|
||||
getRoom(roomName: string, fields?: string): Promise<OpenViduMeetRoom> {
|
||||
let path = `${this.pathPrefix}/${this.apiVersion}/rooms/${roomName}`;
|
||||
if (fields) {
|
||||
path += `?fields=${encodeURIComponent(fields)}`;
|
||||
}
|
||||
return this.getRequest(path);
|
||||
}
|
||||
|
||||
generateParticipantToken(tokenOptions: TokenOptions): Promise<{ token: string }> {
|
||||
|
||||
@ -5,3 +5,4 @@ export * from './global-preferences/global-preferences.service';
|
||||
export * from './room/room.service';
|
||||
export * from './notification/notification.service';
|
||||
export * from './webcomponent-manager/webcomponent-manager.service';
|
||||
export * from './session-storage/session-storage.service';
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SessionStorageService } from './session-storage.service';
|
||||
|
||||
describe('SessionStorageService', () => {
|
||||
let service: SessionStorageService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(SessionStorageService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,81 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
/**
|
||||
* Service for managing session storage operations.
|
||||
* Provides methods to store, retrieve, and remove data from sessionStorage.
|
||||
*/
|
||||
export class SessionStorageService {
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Stores a moderator secret for a specific room.
|
||||
*
|
||||
* @param roomName The room name.
|
||||
* @param secret The secret string.
|
||||
*/
|
||||
public setModeratorSecret(roomName: string, secret: string): void {
|
||||
this.set(`moderator_secret_${roomName}`, secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the moderator secret for a specific room.
|
||||
*
|
||||
* @param roomName The room name.
|
||||
* @returns The stored secret or null if not found.
|
||||
*/
|
||||
public getModeratorSecret(roomName: string): string | null {
|
||||
return this.get<string>(`moderator_secret_${roomName}`) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the moderator secret for a specific room.
|
||||
*
|
||||
* @param roomName The room name.
|
||||
*/
|
||||
public removeModeratorSecret(roomName: string): void {
|
||||
this.remove(`moderator_secret_${roomName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all data stored in sessionStorage.
|
||||
*/
|
||||
public clear(): void {
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a value in sessionStorage.
|
||||
* The value is converted to a JSON string before saving.
|
||||
*
|
||||
* @param key The key under which the value will be stored.
|
||||
* @param value The value to be stored (any type).
|
||||
*/
|
||||
protected set(key: string, value: any): void {
|
||||
const jsonValue = JSON.stringify(value);
|
||||
sessionStorage.setItem(key, jsonValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a value from sessionStorage.
|
||||
* The value is parsed from JSON back to its original type.
|
||||
*
|
||||
* @param key The key of the item to retrieve.
|
||||
* @returns The stored value or null if the key does not exist.
|
||||
*/
|
||||
protected get<T>(key: string): T | null {
|
||||
const jsonValue = sessionStorage.getItem(key);
|
||||
return jsonValue ? (JSON.parse(jsonValue) as T) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a specific item from sessionStorage.
|
||||
*
|
||||
* @param key The key of the item to remove.
|
||||
*/
|
||||
protected remove(key: string): void {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user