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 './validate-room-access.guard';
|
||||||
export * from './application-mode.guard';
|
export * from './application-mode.guard';
|
||||||
export * from './participant-name.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 router = inject(Router);
|
||||||
const contextService = inject(ContextService);
|
const contextService = inject(ContextService);
|
||||||
const roomName = route.params['room-name'];
|
const roomName = route.params['room-name'];
|
||||||
|
const hasParticipantName = !!contextService.getParticipantName();
|
||||||
|
|
||||||
// Check if participant name exists in the service
|
// Check if participant name exists in the service
|
||||||
if (!contextService.getParticipantName()) {
|
if (!hasParticipantName) {
|
||||||
// Redirect to a page where the user can input their participant name
|
// Redirect to a page where the participant can input their participant name
|
||||||
return router.navigate([`${roomName}/participant-name`], {
|
return router.navigate([`${roomName}/participant-name`], {
|
||||||
queryParams: { originUrl: state.url, t: Date.now() },
|
queryParams: { originUrl: state.url, t: Date.now() },
|
||||||
skipLocationChange: true
|
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 { inject } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, CanActivateFn } from '@angular/router';
|
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.
|
* Guard to validate the access to a room.
|
||||||
@ -12,30 +12,38 @@ export const validateRoomAccessGuard: CanActivateFn = async (
|
|||||||
const httpService = inject(HttpService);
|
const httpService = inject(HttpService);
|
||||||
const contextService = inject(ContextService);
|
const contextService = inject(ContextService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
const sessionStorageService = inject(SessionStorageService);
|
||||||
|
|
||||||
const { roomName, participantName, secret } = extractParams(route);
|
const { roomName, participantName, secret } = extractParams(route);
|
||||||
|
const storageSecret = sessionStorageService.getModeratorSecret(roomName);
|
||||||
if (contextService.isEmbeddedMode() && !contextService.getParticipantName()) {
|
|
||||||
await redirectToUnauthorized(router, 'invalid-participant');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Generate a participant token
|
// 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);
|
contextService.setToken(response.token);
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.status === 409) {
|
console.error('Error generating participant token:', error);
|
||||||
// Participant already exists
|
switch (error.status) {
|
||||||
// Redirect to a page where the user can input their participant name
|
case 409:
|
||||||
await router.navigate([`${roomName}/participant-name`], {
|
// Participant already exists.
|
||||||
queryParams: { originUrl: _state.url, accessError: 'participant-exists', t: Date.now() },
|
// Send a timestamp to force update the query params and show the error message in participant name input form
|
||||||
skipLocationChange: true
|
await router.navigate([`${roomName}/participant-name`], {
|
||||||
});
|
queryParams: { originUrl: _state.url, accessError: 'participant-exists', t: Date.now() },
|
||||||
return false;
|
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,
|
validateRoomAccessGuard,
|
||||||
applicationModeGuard,
|
applicationModeGuard,
|
||||||
extractQueryParamsGuard,
|
extractQueryParamsGuard,
|
||||||
checkParticipantNameGuard
|
checkParticipantNameGuard,
|
||||||
|
replaceModeratorSecretGuard
|
||||||
} from '../guards';
|
} from '../guards';
|
||||||
import {
|
import {
|
||||||
AboutComponent,
|
AboutComponent,
|
||||||
@ -77,11 +78,17 @@ export const baseRoutes: Routes = [
|
|||||||
{
|
{
|
||||||
path: ':room-name',
|
path: ':room-name',
|
||||||
component: VideoRoomComponent,
|
component: VideoRoomComponent,
|
||||||
canActivate: [applicationModeGuard, extractQueryParamsGuard, checkParticipantNameGuard, validateRoomAccessGuard],
|
canActivate: [
|
||||||
|
applicationModeGuard,
|
||||||
|
extractQueryParamsGuard,
|
||||||
|
checkParticipantNameGuard,
|
||||||
|
validateRoomAccessGuard,
|
||||||
|
replaceModeratorSecretGuard
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':room-name/participant-name',
|
path: ':room-name/participant-name',
|
||||||
component: ParticipantNameFormComponent,
|
component: ParticipantNameFormComponent
|
||||||
},
|
},
|
||||||
|
|
||||||
// Redirect all other routes to home
|
// Redirect all other routes to home
|
||||||
|
|||||||
@ -23,13 +23,20 @@ export class HttpService {
|
|||||||
return this.deleteRequest(`${this.pathPrefix}/${this.apiVersion}/rooms/${roomName}`);
|
return this.deleteRequest(`${this.pathPrefix}/${this.apiVersion}/rooms/${roomName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
listRooms(): Promise<OpenViduMeetRoom[]> {
|
listRooms(fields?: string): Promise<OpenViduMeetRoom[]> {
|
||||||
//TODO: Add 'fields' query param for filtering rooms by fields
|
let path = `${this.pathPrefix}/${this.apiVersion}/rooms/`;
|
||||||
return this.getRequest(`${this.pathPrefix}/${this.apiVersion}/rooms`);
|
if (fields) {
|
||||||
|
path += `?fields=${encodeURIComponent(fields)}`;
|
||||||
|
}
|
||||||
|
return this.getRequest(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoom(roomName: string): Promise<OpenViduMeetRoom> {
|
getRoom(roomName: string, fields?: string): Promise<OpenViduMeetRoom> {
|
||||||
return this.getRequest(`${this.pathPrefix}/${this.apiVersion}/rooms/${roomName}`);
|
let path = `${this.pathPrefix}/${this.apiVersion}/rooms/${roomName}`;
|
||||||
|
if (fields) {
|
||||||
|
path += `?fields=${encodeURIComponent(fields)}`;
|
||||||
|
}
|
||||||
|
return this.getRequest(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateParticipantToken(tokenOptions: TokenOptions): Promise<{ token: string }> {
|
generateParticipantToken(tokenOptions: TokenOptions): Promise<{ token: string }> {
|
||||||
|
|||||||
@ -5,3 +5,4 @@ export * from './global-preferences/global-preferences.service';
|
|||||||
export * from './room/room.service';
|
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';
|
||||||
|
|||||||
@ -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