frontend: replace UnauthorizedComponent by ErrorComponent and improve error handling in guards

This commit is contained in:
juancarmore 2025-05-21 21:41:42 +02:00
parent b037edb92b
commit ab270239b5
12 changed files with 160 additions and 83 deletions

View File

@ -1,4 +0,0 @@
<div id="unauthorized-content">
<h4>The page you are trying to access is restricted.</h4>
<h4 id="error-reason">Reason: {{ message }}</h4>
</div>

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UnauthorizedComponent } from './unauthorized.component';
describe('UnauthorizedComponent', () => {
let component: UnauthorizedComponent;
let fixture: ComponentFixture<UnauthorizedComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UnauthorizedComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UnauthorizedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,38 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'ov-unauthorized',
standalone: true,
imports: [],
templateUrl: './unauthorized.component.html',
styleUrl: './unauthorized.component.scss'
})
export class UnauthorizedComponent implements OnInit {
message = 'Unauthorized access';
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.queryParams.subscribe((params) => {
const reason = params['reason'];
switch (reason) {
case 'invalid-token':
this.message = 'The token provided is not valid';
break;
case 'invalid-room':
this.message = 'The room name is not valid';
break;
case 'invalid-participant':
this.message = 'The participant name must be provided';
break;
case 'unauthorized-participant':
this.message = 'You are not authorized to join this room';
break;
default:
this.message = 'Unauthorized access';
break;
}
});
}
}

View File

@ -1,7 +1,14 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, RedirectCommand, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService, ContextService, HttpService, SessionStorageService } from '../services';
import {
ActivatedRouteSnapshot,
CanActivateFn,
RedirectCommand,
Router,
RouterStateSnapshot,
UrlTree
} from '@angular/router';
import { AuthMode, ParticipantRole } from '@lib/typings/ce';
import { AuthService, ContextService, HttpService, SessionStorageService } from '../services';
export const checkUserAuthenticatedGuard: CanActivateFn = async (
route: ActivatedRouteSnapshot,
@ -64,9 +71,18 @@ export const checkParticipantRoleAndAuthGuard: CanActivateFn = async (
const roomRoleAndPermissions = await httpService.getRoomRoleAndPermissions(roomId, storageSecret || secret);
participantRole = roomRoleAndPermissions.role;
contextService.setParticipantRole(participantRole);
} catch (error) {
} catch (error: any) {
console.error('Error getting participant role:', error);
return router.createUrlTree(['unauthorized'], { queryParams: { reason: 'unauthorized-participant' } });
switch (error.status) {
case 400:
// Invalid secret
return redirectToErrorPage(router, 'invalid-secret');
case 404:
// Room not found
return redirectToErrorPage(router, 'invalid-room');
default:
return redirectToErrorPage(router, 'internal-error');
}
}
const authMode = await contextService.getAuthModeToEnterRoom();
@ -114,3 +130,7 @@ export const checkUserNotAuthenticatedGuard: CanActivateFn = async (
// Allow access to the requested page
return true;
};
const redirectToErrorPage = (router: Router, reason: string): UrlTree => {
return router.createUrlTree(['error'], { queryParams: { reason } });
};

View File

@ -31,26 +31,29 @@ export const validateRecordingAccessGuard: CanActivateFn = async (
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');
// If the user does not have permission to retrieve recordings, redirect to the error page
return redirectToErrorPage(router, 'unauthorized-recording-access');
}
return true;
} catch (error: any) {
console.error('Error generating recording token:', error);
switch (error.status) {
case 400:
// Invalid secret
return redirectToErrorPage(router, 'invalid-secret');
case 403:
// Recording access is configured for admins only
return redirectToUnauthorized(router, 'unauthorized-recording-access');
return redirectToErrorPage(router, 'recordings-admin-only-access');
case 404:
// There are no recordings in the room or the room does not exist
return redirectToUnauthorized(router, 'no-recordings');
return redirectToErrorPage(router, 'no-recordings');
default:
return redirectToUnauthorized(router, 'invalid-room');
return redirectToErrorPage(router, 'internal-error');
}
}
};
const redirectToUnauthorized = (router: Router, reason: string): UrlTree => {
return router.createUrlTree(['unauthorized'], { queryParams: { reason } });
const redirectToErrorPage = (router: Router, reason: string): UrlTree => {
return router.createUrlTree(['error'], { queryParams: { reason } });
};

View File

@ -38,6 +38,12 @@ export const validateRoomAccessGuard: CanActivateFn = async (
} catch (error: any) {
console.error('Error generating participant token:', error);
switch (error.status) {
case 400:
// Invalid secret
return redirectToErrorPage(router, 'invalid-secret');
case 404:
// Room not found
return redirectToErrorPage(router, 'invalid-room');
case 409:
// Participant already exists.
// Send a timestamp to force update the query params and show the error message in participant name input form
@ -47,14 +53,12 @@ export const validateRoomAccessGuard: CanActivateFn = async (
return new RedirectCommand(participantNameRoute, {
skipLocationChange: true
});
case 406:
return redirectToUnauthorized(router, 'unauthorized-participant');
default:
return redirectToUnauthorized(router, 'invalid-room');
return redirectToErrorPage(router, 'internal-error');
}
}
};
const redirectToUnauthorized = (router: Router, reason: string): UrlTree => {
return router.createUrlTree(['unauthorized'], { queryParams: { reason } });
const redirectToErrorPage = (router: Router, reason: string): UrlTree => {
return router.createUrlTree(['error'], { queryParams: { reason } });
};

View File

@ -0,0 +1,10 @@
<div class="container">
<mat-card class="card">
<mat-card-header>
<mat-card-title>{{ errorName }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>{{ message }}</p>
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,33 @@
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: var(--ov-background-color);
}
.card {
width: 400px;
padding: 20px;
text-align: center;
background-color: var(--ov-surface-color);
border-radius: var(--ov-surface-radius);
}
mat-card-header {
justify-content: center;
align-items: center;
}
mat-card-title {
font-size: 1.5em;
}
mat-card-content p {
font-size: 1em;
color: #555;
}
mat-card-actions {
margin-top: 20px;
}

View File

@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ErrorComponent } from './error.component';
describe('GenericErrorComponent', () => {
let component: ErrorComponent;
let fixture: ComponentFixture<ErrorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ErrorComponent]
}).compileComponents();
fixture = TestBed.createComponent(ErrorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,50 @@
import { Component, OnInit } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'ov-error',
standalone: true,
imports: [MatCardModule],
templateUrl: './error.component.html',
styleUrl: './error.component.scss'
})
export class ErrorComponent implements OnInit {
errorName = 'Error';
message = '';
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.queryParams.subscribe((params) => {
const reason = params['reason'];
switch (reason) {
case 'invalid-secret':
this.errorName = 'Invalid secret';
this.message =
'The secret provided to join the room is neither valid for moderators nor publishers';
break;
case 'invalid-room':
this.errorName = 'Invalid room';
this.message = 'The room you are trying to join does not exist or has been deleted';
break;
case 'no-recordings':
this.errorName = 'No recordings';
this.message = 'There are no recordings in this room or the room does not exist';
break;
case 'unauthorized-recording-access':
this.errorName = 'Unauthorized recording access';
this.message = 'You are not authorized to access the recordings in this room';
break;
case 'recordings-admin-only-access':
this.errorName = 'Unauthorized recording access';
this.message = 'Recordings access is configured for admins only in this room';
break;
default:
this.errorName = 'Internal error';
this.message = 'Something went wrong...';
break;
}
});
}
}

View File

@ -1,6 +1,5 @@
import { Routes } from '@angular/router';
import { UserRole } from '@lib/typings/ce';
import { RoomCreatorDisabledComponent, UnauthorizedComponent } from '../components';
import {
applicationModeGuard,
checkParticipantNameGuard,
@ -20,11 +19,13 @@ import {
ConsoleComponent,
ConsoleLoginComponent,
DisconnectedComponent,
ErrorComponent,
LoginComponent,
OverviewComponent,
ParticipantNameFormComponent,
RecordingsComponent,
RoomCreatorComponent,
RoomCreatorDisabledComponent,
RoomFormComponent,
RoomRecordingsComponent,
RoomsComponent,
@ -51,7 +52,7 @@ export const baseRoutes: Routes = [
},
{ path: 'room-creator-disabled', component: RoomCreatorDisabledComponent },
{ path: 'disconnected', component: DisconnectedComponent },
{ path: 'unauthorized', component: UnauthorizedComponent },
{ path: 'error', component: ErrorComponent },
{
path: 'console/login',
component: ConsoleLoginComponent,