From 28b65db651caf459ae91ce065b741df08ea91fc8 Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 28 Mar 2025 12:15:11 +0100 Subject: [PATCH] frontend: Implement participant role retrieval and enhance authentication guards and http interceptor --- .../src/lib/guards/auth.guard.ts | 29 +++++++++++++++---- .../src/lib/interceptors/http.interceptor.ts | 22 ++++++++++---- .../src/lib/pages/login/login.component.ts | 3 +- .../src/lib/routes/base-routes.ts | 9 ++---- .../src/lib/services/http/http.service.ts | 18 ++++++++++-- 5 files changed, 58 insertions(+), 23 deletions(-) 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 f4fe82d..715e72e 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 @@ -1,6 +1,6 @@ import { inject } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; -import { AuthService, ContextService } from '../services'; +import { ActivatedRouteSnapshot, CanActivateFn, RedirectCommand, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthService, ContextService, HttpService, SessionStorageService } from '../services'; import { AuthMode, ParticipantRole } from '@lib/typings/ce'; export const checkUserAuthenticatedGuard: CanActivateFn = async ( @@ -47,11 +47,26 @@ export const checkParticipantRoleAndAuthGuard: CanActivateFn = async ( _route: ActivatedRouteSnapshot, state: RouterStateSnapshot ) => { + const router = inject(Router); const authService = inject(AuthService); const contextService = inject(ContextService); - const router = inject(Router); + const sessionStorageService = inject(SessionStorageService); + const httpService = inject(HttpService); + + // Get participant role by room secret + let participantRole: ParticipantRole; + + try { + const roomName = contextService.getRoomName(); + const secret = contextService.getSecret(); + const storageSecret = sessionStorageService.getModeratorSecret(roomName); + + participantRole = await httpService.getParticipantRole(roomName, storageSecret || secret); + } catch (error) { + console.error('Error getting participant role:', error); + return router.createUrlTree(['unauthorized'], { queryParams: { reason: 'unauthorized-participant' } }); + } - const participantRole = contextService.getParticipantRole(); const authMode = await contextService.getAuthModeToEnterRoom(); // If the user is a moderator and the room requires authentication for moderators only, @@ -60,16 +75,18 @@ export const checkParticipantRoleAndAuthGuard: CanActivateFn = async ( const isAuthRequiredForModerators = authMode === AuthMode.MODERATORS_ONLY && participantRole === ParticipantRole.MODERATOR; const isAuthRequiredForAllUsers = authMode === AuthMode.ALL_USERS; - console.log('Participant role:', participantRole); if (isAuthRequiredForModerators || isAuthRequiredForAllUsers) { // Check if user is authenticated const isAuthenticated = await authService.isUserAuthenticated(); if (!isAuthenticated) { // Redirect to the login page with query param to redirect back to the room - return router.createUrlTree(['login'], { + const loginRoute = router.createUrlTree(['login'], { queryParams: { redirectTo: state.url } }); + return new RedirectCommand(loginRoute, { + skipLocationChange: true + }); } } diff --git a/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts b/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts index e710905..ae4d392 100644 --- a/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts +++ b/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts @@ -11,7 +11,8 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, ne const sessionStorageService = inject(SessionStorageService); const httpService: HttpService = inject(HttpService); - const url = router.getCurrentNavigation()?.finalUrl?.toString() || router.url; + const pageUrl = router.getCurrentNavigation()?.finalUrl?.toString() || router.url; + const requestUrl = req.url; req = req.clone({ withCredentials: true @@ -26,9 +27,15 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, ne }), catchError((error: HttpErrorResponse) => { if (error.url?.includes('/auth/refresh')) { - console.error('Error refreshing access token. Logging out...'); - const redirectTo = url.startsWith('/console') ? 'console/login' : 'login'; - authService.logout(redirectTo); + console.error('Error refreshing access token'); + + // If the original request was not to the profile endpoint, logout and redirect to the login page + if (!requestUrl.includes('/profile')) { + console.log('Logging out...'); + const redirectTo = pageUrl.startsWith('/console') ? 'console/login' : 'login'; + authService.logout(redirectTo); + } + throw firstError; } @@ -79,9 +86,12 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, ne } // Expired access/participant token - if (url.startsWith('/room')) { + if (pageUrl.startsWith('/room') && !requestUrl.includes('/profile')) { + // If the error occurred in a room page and the request is not to the profile endpoint, + // refresh the participant token return refreshParticipantToken(error); - } else if (!url.startsWith('/console/login') && !url.startsWith('/login')) { + } else if (!pageUrl.startsWith('/console/login') && !pageUrl.startsWith('/login')) { + // If the error occurred in a page that is not the login page, refresh the access token return refreshAccessToken(error); } } diff --git a/frontend/projects/shared-meet-components/src/lib/pages/login/login.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/login/login.component.ts index e380144..ddf28c6 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/login/login.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/login/login.component.ts @@ -63,7 +63,8 @@ export class LoginComponent { return; } - this.router.navigate([this.redirectTo]); + let urlTree = this.router.parseUrl(this.redirectTo); + this.router.navigateByUrl(urlTree); } catch (error) { if ((error as HttpErrorResponse).status === 429) { this.loginErrorMessage = 'Too many login attempts. Please try again later'; 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 fc537d2..c216657 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 @@ -35,12 +35,7 @@ export const baseRoutes: Routes = [ { path: '', component: RoomCreatorComponent, - canActivate: [ - runGuardsSerially( - checkRoomCreatorEnabledGuard, - checkUserAuthenticatedGuard - ) - ], + canActivate: [runGuardsSerially(checkRoomCreatorEnabledGuard, checkUserAuthenticatedGuard)], data: { checkSkipAuth: true, expectedRoles: [UserRole.USER], @@ -121,9 +116,9 @@ export const baseRoutes: Routes = [ runGuardsSerially( applicationModeGuard, extractQueryParamsGuard, + checkParticipantRoleAndAuthGuard, checkParticipantNameGuard, validateRoomAccessGuard, - checkParticipantRoleAndAuthGuard, replaceModeratorSecretGuard ) ] diff --git a/frontend/projects/shared-meet-components/src/lib/services/http/http.service.ts b/frontend/projects/shared-meet-components/src/lib/services/http/http.service.ts index 9a9f0cc..a9e06bf 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/http/http.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/http/http.service.ts @@ -1,7 +1,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { OpenViduMeetRoom, OpenViduMeetRoomOptions } from 'projects/shared-meet-components/src/lib/typings/ce/room'; -import { GlobalPreferences, RoomPreferences, TokenOptions, User } from '@lib/typings/ce'; +import { GlobalPreferences, ParticipantRole, RoomPreferences, TokenOptions, User } from '@lib/typings/ce'; import { RecordingInfo, Room } from 'openvidu-components-angular'; import { lastValueFrom } from 'rxjs'; @@ -40,12 +40,24 @@ export class HttpService { return this.getRequest(path); } + getParticipantRole(roomName: string, secret: string): Promise { + return this.getRequest( + `${this.INTERNAL_API_PATH_PREFIX}/${this.API_V1_VERSION}/rooms/${roomName}/participant-role?secret=${secret}` + ); + } + generateParticipantToken(tokenOptions: TokenOptions): Promise<{ token: string }> { - return this.postRequest(`${this.INTERNAL_API_PATH_PREFIX}/${this.API_V1_VERSION}/participants/token`, tokenOptions); + return this.postRequest( + `${this.INTERNAL_API_PATH_PREFIX}/${this.API_V1_VERSION}/participants/token`, + tokenOptions + ); } refreshParticipantToken(tokenOptions: TokenOptions): Promise<{ token: string }> { - return this.postRequest(`${this.INTERNAL_API_PATH_PREFIX}/${this.API_V1_VERSION}/participants/token/refresh`, tokenOptions); + return this.postRequest( + `${this.INTERNAL_API_PATH_PREFIX}/${this.API_V1_VERSION}/participants/token/refresh`, + tokenOptions + ); } /**