frontend: Add new route to access RoomRecordingsComponent and associated guards

This commit is contained in:
juancarmore 2025-05-21 18:58:11 +02:00
parent b4292e8ca1
commit 6a9cd04a74
10 changed files with 172 additions and 37 deletions

View File

@ -25,8 +25,8 @@ export const checkUserAuthenticatedGuard: CanActivateFn = async (
const isAuthenticated = await authService.isUserAuthenticated(); const isAuthenticated = await authService.isUserAuthenticated();
if (!isAuthenticated) { if (!isAuthenticated) {
// Redirect to the login page specified in the route data when user is not authenticated // Redirect to the login page specified in the route data when user is not authenticated
const { redirectToUnauthorized } = route.data; const { redirectToWhenUnauthorized } = route.data;
return router.createUrlTree([redirectToUnauthorized]); return router.createUrlTree([redirectToWhenUnauthorized]);
} }
// Check if the user has the expected roles // Check if the user has the expected roles
@ -35,8 +35,8 @@ export const checkUserAuthenticatedGuard: CanActivateFn = async (
if (!expectedRoles.includes(userRole)) { if (!expectedRoles.includes(userRole)) {
// Redirect to the page specified in the route data when user has an invalid role // Redirect to the page specified in the route data when user has an invalid role
const { redirectToInvalidRole } = route.data; const { redirectToWhenInvalidRole } = route.data;
return router.createUrlTree([redirectToInvalidRole]); return router.createUrlTree([redirectToWhenInvalidRole]);
} }
// Allow access to the requested page // Allow access to the requested page
@ -63,6 +63,7 @@ export const checkParticipantRoleAndAuthGuard: CanActivateFn = async (
const roomRoleAndPermissions = await httpService.getRoomRoleAndPermissions(roomId, storageSecret || secret); const roomRoleAndPermissions = await httpService.getRoomRoleAndPermissions(roomId, storageSecret || secret);
participantRole = roomRoleAndPermissions.role; participantRole = roomRoleAndPermissions.role;
contextService.setParticipantRole(participantRole);
} catch (error) { } catch (error) {
console.error('Error getting participant role:', error); console.error('Error getting participant role:', error);
return router.createUrlTree(['unauthorized'], { queryParams: { reason: 'unauthorized-participant' } }); return router.createUrlTree(['unauthorized'], { queryParams: { reason: 'unauthorized-participant' } });

View File

@ -1,10 +1,11 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';
import { ContextService } from '../services'; import { ContextService } from '../services';
export const extractQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
const router = inject(Router);
const contextService = inject(ContextService); const contextService = inject(ContextService);
const { roomId, participantName, secret, leaveRedirectUrl } = extractParams(route); const { roomId, participantName, secret, leaveRedirectUrl, viewRecordings } = extractParams(route);
if (isValidUrl(leaveRedirectUrl)) { if (isValidUrl(leaveRedirectUrl)) {
contextService.setLeaveRedirectUrl(leaveRedirectUrl); contextService.setLeaveRedirectUrl(leaveRedirectUrl);
@ -14,6 +15,23 @@ export const extractQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnap
contextService.setParticipantName(participantName); contextService.setParticipantName(participantName);
contextService.setSecret(secret); contextService.setSecret(secret);
if (viewRecordings === 'true') {
// Redirect to the room recordings page
return router.createUrlTree([`room/${roomId}/recordings`], {
queryParams: { secret }
});
}
return true;
};
export const extractRecordingQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
const contextService = inject(ContextService);
const { roomId, secret } = extractParams(route);
contextService.setRoomId(roomId);
contextService.setSecret(secret);
return true; return true;
}; };
@ -21,7 +39,8 @@ const extractParams = (route: ActivatedRouteSnapshot) => ({
roomId: route.params['room-id'], roomId: route.params['room-id'],
participantName: route.queryParams['participant-name'], participantName: route.queryParams['participant-name'],
secret: route.queryParams['secret'], secret: route.queryParams['secret'],
leaveRedirectUrl: route.queryParams['leave-redirect-url'] leaveRedirectUrl: route.queryParams['leave-redirect-url'],
viewRecordings: route.queryParams['view-recordings']
}); });
const isValidUrl = (url: string) => { const isValidUrl = (url: string) => {

View File

@ -1,8 +1,9 @@
export * from './auth.guard'; export * from './auth.guard';
export * from './extract-query-params.guard'; export * from './extract-query-params.guard';
export * from './validate-room-access.guard'; export * from './validate-room-access.guard';
export * from './validate-recording-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'; export * from './moderator-secret.guard';
export * from './room-creator.guard'; export * from './room-creator.guard';
export * from './run-serially.guard'; export * from './run-serially.guard';

View File

@ -53,6 +53,44 @@ export const replaceModeratorSecretGuard: CanActivateFn = (route, _state) => {
} }
}; };
/**
* Guard that intercepts navigation to remove the 'secret' query parameter from the URL
* when a moderator participant is detected. The secret is stored in session storage
* for the current room, and the URL is updated without the 'secret' parameter to
* enhance security.
*/
export const removeModeratorSecretGuard: CanActivateFn = (route, _state) => {
const httpService = inject(HttpService);
const contextService = inject(ContextService);
const router = inject(Router);
const location: Location = inject(Location);
const sessionStorageService = inject(SessionStorageService);
router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
take(1)
)
.subscribe(async () => {
if (contextService.isModeratorParticipant()) {
const roomId = contextService.getRoomId();
const storedSecret = sessionStorageService.getModeratorSecret(roomId);
const moderatorSecret = storedSecret || contextService.getSecret();
sessionStorageService.setModeratorSecret(roomId, moderatorSecret);
// Remove secret from URL
const queryParams = { ...route.queryParams };
delete queryParams['secret'];
const urlTree = router.createUrlTree([], { queryParams });
const newUrl = router.serializeUrl(urlTree);
location.replaceState(newUrl);
}
});
return true;
};
const getUrlSecret = async ( const getUrlSecret = async (
httpService: HttpService, httpService: HttpService,
roomId: string roomId: string

View File

@ -3,10 +3,10 @@ import { CanActivateFn, RedirectCommand } from '@angular/router';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ContextService } from '../services'; import { ContextService } from '../services';
export const checkParticipantNameGuard: CanActivateFn = (route, state) => { export const checkParticipantNameGuard: CanActivateFn = (_route, state) => {
const router = inject(Router); const router = inject(Router);
const contextService = inject(ContextService); const contextService = inject(ContextService);
const roomId = route.params['room-id']; const roomId = contextService.getRoomId();
const hasParticipantName = !!contextService.getParticipantName(); const hasParticipantName = !!contextService.getParticipantName();
// Check if participant name exists in the service // Check if participant name exists in the service

View File

@ -0,0 +1,56 @@
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
CanActivateFn,
UrlTree,
RedirectCommand
} from '@angular/router';
import { ContextService, HttpService, SessionStorageService } from '../services';
/**
* Guard to validate the access to recordings.
*/
export const validateRecordingAccessGuard: CanActivateFn = async (
_route: ActivatedRouteSnapshot,
_state: RouterStateSnapshot
) => {
const httpService = inject(HttpService);
const contextService = inject(ContextService);
const router = inject(Router);
const sessionStorageService = inject(SessionStorageService);
const roomId = contextService.getRoomId();
const secret = contextService.getSecret();
const storageSecret = sessionStorageService.getModeratorSecret(roomId);
try {
// Generate a token to access recordings in the room
const response = await httpService.generateRecordingToken(roomId, storageSecret || secret);
contextService.setRecordingPermissionsFromToken(response.token);
if (!contextService.canRetrieveRecordings()) {
// If the user does not have permission to retrieve recordings, redirect to the unauthorized page
return redirectToUnauthorized(router, 'unauthorized-recording-access');
}
return true;
} catch (error: any) {
console.error('Error generating recording token:', error);
switch (error.status) {
case 403:
// Recording access is configured for admins only
return redirectToUnauthorized(router, 'unauthorized-recording-access');
case 404:
// There are no recordings in the room or the room does not exist
return redirectToUnauthorized(router, 'no-recordings');
default:
return redirectToUnauthorized(router, 'invalid-room');
}
}
};
const redirectToUnauthorized = (router: Router, reason: string): UrlTree => {
return router.createUrlTree(['unauthorized'], { queryParams: { reason } });
};

View File

@ -1,5 +1,12 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, CanActivateFn, UrlTree, RedirectCommand } from '@angular/router'; import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
CanActivateFn,
UrlTree,
RedirectCommand
} from '@angular/router';
import { ContextService, HttpService, SessionStorageService } from '../services'; import { ContextService, HttpService, SessionStorageService } from '../services';
/** /**
@ -26,7 +33,7 @@ export const validateRoomAccessGuard: CanActivateFn = async (
participantName, participantName,
secret: storageSecret || secret secret: storageSecret || secret
}); });
contextService.setToken(response.token); contextService.setParticipantToken(response.token);
return true; return true;
} catch (error: any) { } catch (error: any) {
console.error('Error generating participant token:', error); console.error('Error generating participant token:', error);

View File

@ -82,9 +82,9 @@ export class RoomCreatorComponent implements OnInit {
const accessRoomUrl = new URL(room.moderatorRoomUrl); const accessRoomUrl = new URL(room.moderatorRoomUrl);
const secret = accessRoomUrl.searchParams.get('secret'); const secret = accessRoomUrl.searchParams.get('secret');
const roomId = accessRoomUrl.pathname; const roomUrl = accessRoomUrl.pathname;
this.router.navigate([roomId], { queryParams: { secret } }); this.router.navigate([roomUrl], { queryParams: { secret } });
} catch (error) { } catch (error) {
console.error('Error creating room ', error); console.error('Error creating room ', error);
} }

View File

@ -105,7 +105,7 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.ctxService.setParticipantName(participantName); this.ctxService.setParticipantName(participantName);
} }
this.token = this.ctxService.getToken(); this.token = this.ctxService.getParticipantToken();
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
this.serverError = error.error; this.serverError = error.error;

View File

@ -1,35 +1,35 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { UnauthorizedComponent, RoomCreatorDisabledComponent } from '../components'; import { UserRole } from '@lib/typings/ce';
import { RoomCreatorDisabledComponent, UnauthorizedComponent } from '../components';
import { import {
applicationModeGuard,
checkParticipantNameGuard,
checkParticipantRoleAndAuthGuard,
checkRoomCreatorEnabledGuard,
checkUserAuthenticatedGuard, checkUserAuthenticatedGuard,
checkUserNotAuthenticatedGuard, checkUserNotAuthenticatedGuard,
validateRoomAccessGuard, extractRecordingQueryParamsGuard,
applicationModeGuard, extractRoomQueryParamsGuard,
extractQueryParamsGuard, removeModeratorSecretGuard,
checkParticipantNameGuard,
replaceModeratorSecretGuard, replaceModeratorSecretGuard,
checkRoomCreatorEnabledGuard, runGuardsSerially,
checkParticipantRoleAndAuthGuard, validateRecordingAccessGuard,
runGuardsSerially validateRoomAccessGuard
} from '../guards'; } from '../guards';
import { import {
AboutComponent,
AccessPermissionsComponent,
AppearanceComponent,
ConsoleComponent, ConsoleComponent,
ConsoleLoginComponent, ConsoleLoginComponent,
DisconnectedComponent, DisconnectedComponent,
RoomCreatorComponent,
LoginComponent, LoginComponent,
OverviewComponent, OverviewComponent,
ParticipantNameFormComponent, ParticipantNameFormComponent,
RecordingsComponent, RecordingsComponent,
RoomCreatorComponent,
RoomFormComponent,
RoomRecordingsComponent,
RoomsComponent, RoomsComponent,
SecurityPreferencesComponent, VideoRoomComponent
VideoRoomComponent,
RoomFormComponent
} from '../pages'; } from '../pages';
import { UserRole } from '@lib/typings/ce';
export const baseRoutes: Routes = [ export const baseRoutes: Routes = [
{ {
@ -39,8 +39,8 @@ export const baseRoutes: Routes = [
data: { data: {
checkSkipAuth: true, checkSkipAuth: true,
expectedRoles: [UserRole.USER], expectedRoles: [UserRole.USER],
redirectToUnauthorized: 'login', redirectToWhenUnauthorized: 'login',
redirectToInvalidRole: 'console' redirectToWhenInvalidRole: 'console'
} }
}, },
{ {
@ -64,8 +64,8 @@ export const baseRoutes: Routes = [
canActivate: [checkUserAuthenticatedGuard], canActivate: [checkUserAuthenticatedGuard],
data: { data: {
expectedRoles: [UserRole.ADMIN], expectedRoles: [UserRole.ADMIN],
redirectToUnauthorized: 'console/login', redirectToWhenUnauthorized: 'console/login',
redirectToInvalidRole: '' redirectToWhenInvalidRole: ''
}, },
children: [ children: [
{ {
@ -115,7 +115,7 @@ export const baseRoutes: Routes = [
canActivate: [ canActivate: [
runGuardsSerially( runGuardsSerially(
applicationModeGuard, applicationModeGuard,
extractQueryParamsGuard, extractRoomQueryParamsGuard,
checkParticipantRoleAndAuthGuard, checkParticipantRoleAndAuthGuard,
checkParticipantNameGuard, checkParticipantNameGuard,
validateRoomAccessGuard, validateRoomAccessGuard,
@ -127,6 +127,19 @@ export const baseRoutes: Routes = [
path: 'room/:room-id/participant-name', path: 'room/:room-id/participant-name',
component: ParticipantNameFormComponent component: ParticipantNameFormComponent
}, },
{
path: 'room/:room-id/recordings',
component: RoomRecordingsComponent,
canActivate: [
runGuardsSerially(
applicationModeGuard,
extractRecordingQueryParamsGuard,
checkParticipantRoleAndAuthGuard,
validateRecordingAccessGuard,
removeModeratorSecretGuard
)
]
},
// Redirect all other routes to RoomCreatorComponent // Redirect all other routes to RoomCreatorComponent
{ path: '**', redirectTo: '' } { path: '**', redirectTo: '' }