From 6a9cd04a74c77df382a39ce82ee0fd0ab1a8abfb Mon Sep 17 00:00:00 2001 From: juancarmore Date: Wed, 21 May 2025 18:58:11 +0200 Subject: [PATCH] frontend: Add new route to access RoomRecordingsComponent and associated guards --- .../src/lib/guards/auth.guard.ts | 9 +-- .../lib/guards/extract-query-params.guard.ts | 27 +++++++-- .../src/lib/guards/index.ts | 3 +- ...ret.guard.ts => moderator-secret.guard.ts} | 38 +++++++++++++ .../src/lib/guards/participant-name.guard.ts | 4 +- .../guards/validate-recording-access.guard.ts | 56 +++++++++++++++++++ .../lib/guards/validate-room-access.guard.ts | 11 +++- .../room-creator/room-creator.component.ts | 4 +- .../pages/video-room/video-room.component.ts | 2 +- .../src/lib/routes/base-routes.ts | 55 +++++++++++------- 10 files changed, 172 insertions(+), 37 deletions(-) rename frontend/projects/shared-meet-components/src/lib/guards/{replace-moderator-secret.guard.ts => moderator-secret.guard.ts} (67%) create mode 100644 frontend/projects/shared-meet-components/src/lib/guards/validate-recording-access.guard.ts diff --git a/frontend/projects/shared-meet-components/src/lib/guards/auth.guard.ts b/frontend/projects/shared-meet-components/src/lib/guards/auth.guard.ts index cf81e47..5a88c7c 100644 --- a/frontend/projects/shared-meet-components/src/lib/guards/auth.guard.ts +++ b/frontend/projects/shared-meet-components/src/lib/guards/auth.guard.ts @@ -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' } }); diff --git a/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts b/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts index 80e01bf..f8ed3fb 100644 --- a/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts +++ b/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts @@ -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) => { diff --git a/frontend/projects/shared-meet-components/src/lib/guards/index.ts b/frontend/projects/shared-meet-components/src/lib/guards/index.ts index eb8bf25..6df5b3f 100644 --- a/frontend/projects/shared-meet-components/src/lib/guards/index.ts +++ b/frontend/projects/shared-meet-components/src/lib/guards/index.ts @@ -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'; diff --git a/frontend/projects/shared-meet-components/src/lib/guards/replace-moderator-secret.guard.ts b/frontend/projects/shared-meet-components/src/lib/guards/moderator-secret.guard.ts similarity index 67% rename from frontend/projects/shared-meet-components/src/lib/guards/replace-moderator-secret.guard.ts rename to frontend/projects/shared-meet-components/src/lib/guards/moderator-secret.guard.ts index f131dd2..0301f51 100644 --- a/frontend/projects/shared-meet-components/src/lib/guards/replace-moderator-secret.guard.ts +++ b/frontend/projects/shared-meet-components/src/lib/guards/moderator-secret.guard.ts @@ -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 diff --git a/frontend/projects/shared-meet-components/src/lib/guards/participant-name.guard.ts b/frontend/projects/shared-meet-components/src/lib/guards/participant-name.guard.ts index 374207f..1ece264 100644 --- a/frontend/projects/shared-meet-components/src/lib/guards/participant-name.guard.ts +++ b/frontend/projects/shared-meet-components/src/lib/guards/participant-name.guard.ts @@ -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 diff --git a/frontend/projects/shared-meet-components/src/lib/guards/validate-recording-access.guard.ts b/frontend/projects/shared-meet-components/src/lib/guards/validate-recording-access.guard.ts new file mode 100644 index 0000000..ae215a0 --- /dev/null +++ b/frontend/projects/shared-meet-components/src/lib/guards/validate-recording-access.guard.ts @@ -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 } }); +}; diff --git a/frontend/projects/shared-meet-components/src/lib/guards/validate-room-access.guard.ts b/frontend/projects/shared-meet-components/src/lib/guards/validate-room-access.guard.ts index 13d3a33..39cf8f6 100644 --- a/frontend/projects/shared-meet-components/src/lib/guards/validate-room-access.guard.ts +++ b/frontend/projects/shared-meet-components/src/lib/guards/validate-room-access.guard.ts @@ -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); diff --git a/frontend/projects/shared-meet-components/src/lib/pages/room-creator/room-creator.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/room-creator/room-creator.component.ts index 8ea2a63..d2893ea 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/room-creator/room-creator.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/room-creator/room-creator.component.ts @@ -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); } diff --git a/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.ts index 87583f0..916ab63 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.ts @@ -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; diff --git a/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts b/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts index 451864f..7fdcf11 100644 --- a/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts +++ b/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts @@ -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: '' }