frontend: Implement participant role retrieval and enhance authentication guards and http interceptor

This commit is contained in:
juancarmore 2025-03-28 12:15:11 +01:00
parent bc33e9c5d9
commit 28b65db651
5 changed files with 58 additions and 23 deletions

View File

@ -1,6 +1,6 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivateFn, RedirectCommand, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService, ContextService } from '../services'; import { AuthService, ContextService, HttpService, SessionStorageService } from '../services';
import { AuthMode, ParticipantRole } from '@lib/typings/ce'; import { AuthMode, ParticipantRole } from '@lib/typings/ce';
export const checkUserAuthenticatedGuard: CanActivateFn = async ( export const checkUserAuthenticatedGuard: CanActivateFn = async (
@ -47,11 +47,26 @@ export const checkParticipantRoleAndAuthGuard: CanActivateFn = async (
_route: ActivatedRouteSnapshot, _route: ActivatedRouteSnapshot,
state: RouterStateSnapshot state: RouterStateSnapshot
) => { ) => {
const router = inject(Router);
const authService = inject(AuthService); const authService = inject(AuthService);
const contextService = inject(ContextService); 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(); const authMode = await contextService.getAuthModeToEnterRoom();
// If the user is a moderator and the room requires authentication for moderators only, // 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 = const isAuthRequiredForModerators =
authMode === AuthMode.MODERATORS_ONLY && participantRole === ParticipantRole.MODERATOR; authMode === AuthMode.MODERATORS_ONLY && participantRole === ParticipantRole.MODERATOR;
const isAuthRequiredForAllUsers = authMode === AuthMode.ALL_USERS; const isAuthRequiredForAllUsers = authMode === AuthMode.ALL_USERS;
console.log('Participant role:', participantRole);
if (isAuthRequiredForModerators || isAuthRequiredForAllUsers) { if (isAuthRequiredForModerators || isAuthRequiredForAllUsers) {
// Check if user is authenticated // Check if user is authenticated
const isAuthenticated = await authService.isUserAuthenticated(); const isAuthenticated = await authService.isUserAuthenticated();
if (!isAuthenticated) { if (!isAuthenticated) {
// Redirect to the login page with query param to redirect back to the room // 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 } queryParams: { redirectTo: state.url }
}); });
return new RedirectCommand(loginRoute, {
skipLocationChange: true
});
} }
} }

View File

@ -11,7 +11,8 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, ne
const sessionStorageService = inject(SessionStorageService); const sessionStorageService = inject(SessionStorageService);
const httpService: HttpService = inject(HttpService); 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({ req = req.clone({
withCredentials: true withCredentials: true
@ -26,9 +27,15 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, ne
}), }),
catchError((error: HttpErrorResponse) => { catchError((error: HttpErrorResponse) => {
if (error.url?.includes('/auth/refresh')) { if (error.url?.includes('/auth/refresh')) {
console.error('Error refreshing access token. Logging out...'); console.error('Error refreshing access token');
const redirectTo = url.startsWith('/console') ? 'console/login' : 'login';
authService.logout(redirectTo); // 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; throw firstError;
} }
@ -79,9 +86,12 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, ne
} }
// Expired access/participant token // 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); 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); return refreshAccessToken(error);
} }
} }

View File

@ -63,7 +63,8 @@ export class LoginComponent {
return; return;
} }
this.router.navigate([this.redirectTo]); let urlTree = this.router.parseUrl(this.redirectTo);
this.router.navigateByUrl(urlTree);
} catch (error) { } catch (error) {
if ((error as HttpErrorResponse).status === 429) { if ((error as HttpErrorResponse).status === 429) {
this.loginErrorMessage = 'Too many login attempts. Please try again later'; this.loginErrorMessage = 'Too many login attempts. Please try again later';

View File

@ -35,12 +35,7 @@ export const baseRoutes: Routes = [
{ {
path: '', path: '',
component: RoomCreatorComponent, component: RoomCreatorComponent,
canActivate: [ canActivate: [runGuardsSerially(checkRoomCreatorEnabledGuard, checkUserAuthenticatedGuard)],
runGuardsSerially(
checkRoomCreatorEnabledGuard,
checkUserAuthenticatedGuard
)
],
data: { data: {
checkSkipAuth: true, checkSkipAuth: true,
expectedRoles: [UserRole.USER], expectedRoles: [UserRole.USER],
@ -121,9 +116,9 @@ export const baseRoutes: Routes = [
runGuardsSerially( runGuardsSerially(
applicationModeGuard, applicationModeGuard,
extractQueryParamsGuard, extractQueryParamsGuard,
checkParticipantRoleAndAuthGuard,
checkParticipantNameGuard, checkParticipantNameGuard,
validateRoomAccessGuard, validateRoomAccessGuard,
checkParticipantRoleAndAuthGuard,
replaceModeratorSecretGuard replaceModeratorSecretGuard
) )
] ]

View File

@ -1,7 +1,7 @@
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { OpenViduMeetRoom, OpenViduMeetRoomOptions } from 'projects/shared-meet-components/src/lib/typings/ce/room'; 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 { RecordingInfo, Room } from 'openvidu-components-angular';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
@ -40,12 +40,24 @@ export class HttpService {
return this.getRequest(path); return this.getRequest(path);
} }
getParticipantRole(roomName: string, secret: string): Promise<ParticipantRole> {
return this.getRequest(
`${this.INTERNAL_API_PATH_PREFIX}/${this.API_V1_VERSION}/rooms/${roomName}/participant-role?secret=${secret}`
);
}
generateParticipantToken(tokenOptions: TokenOptions): Promise<{ token: string }> { 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 }> { 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
);
} }
/** /**