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

View File

@ -1,10 +1,11 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';
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 { roomId, participantName, secret, leaveRedirectUrl } = extractParams(route);
const { roomId, participantName, secret, leaveRedirectUrl, viewRecordings } = extractParams(route);
if (isValidUrl(leaveRedirectUrl)) {
contextService.setLeaveRedirectUrl(leaveRedirectUrl);
@ -14,6 +15,23 @@ export const extractQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnap
contextService.setParticipantName(participantName);
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;
};
@ -21,7 +39,8 @@ const extractParams = (route: ActivatedRouteSnapshot) => ({
roomId: route.params['room-id'],
participantName: route.queryParams['participant-name'],
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) => {

View File

@ -1,8 +1,9 @@
export * from './auth.guard';
export * from './extract-query-params.guard';
export * from './validate-room-access.guard';
export * from './validate-recording-access.guard';
export * from './application-mode.guard';
export * from './participant-name.guard';
export * from './replace-moderator-secret.guard';
export * from './moderator-secret.guard';
export * from './room-creator.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 (
httpService: HttpService,
roomId: string

View File

@ -3,10 +3,10 @@ import { CanActivateFn, RedirectCommand } from '@angular/router';
import { Router } from '@angular/router';
import { ContextService } from '../services';
export const checkParticipantNameGuard: CanActivateFn = (route, state) => {
export const checkParticipantNameGuard: CanActivateFn = (_route, state) => {
const router = inject(Router);
const contextService = inject(ContextService);
const roomId = route.params['room-id'];
const roomId = contextService.getRoomId();
const hasParticipantName = !!contextService.getParticipantName();
// 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 { 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';
/**
@ -26,7 +33,7 @@ export const validateRoomAccessGuard: CanActivateFn = async (
participantName,
secret: storageSecret || secret
});
contextService.setToken(response.token);
contextService.setParticipantToken(response.token);
return true;
} catch (error: any) {
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 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) {
console.error('Error creating room ', error);
}

View File

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

View File

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