Add admin page and fix race condition in AuthorizeGuard

This commit is contained in:
Matteo Gheza 2024-01-10 00:26:14 +01:00
parent 350e0b22bd
commit 5b109da648
22 changed files with 295 additions and 12 deletions

View File

@ -18,7 +18,10 @@ return [
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'c,r,u',
'logs' => 'r'
'logs' => 'r',
'admin' => 'r',
'admin-info' => 'r,u',
'admin-roles' => 'r,u'
],
'admin' => [
'users' => 'c,r,u,d,i,b,h,sc,sd,atc,ame',
@ -26,7 +29,10 @@ return [
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'c,r,u',
'logs' => 'lr'
'logs' => 'lr',
'admin' => 'r',
'admin-info' => 'r,u',
'admin-roles' => 'r,u'
],
'chief' => [
'users' => 'r,u,sc,sd,atc,ame',

View File

@ -49,7 +49,7 @@ export class AuthorizeGuard {
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
console.log(this.router.url, route, state);
if(this.authService.authLoaded) {
if(this.authService.authLoaded()) {
this.guardLoaderIconService.hide();
console.log("Auth already loaded, checking if profile id exists");
return this.checkAuthAndRedirect(route, state);
@ -57,12 +57,26 @@ export class AuthorizeGuard {
this.guardLoaderIconService.show();
console.log("Auth not loaded, waiting for authChanged");
return new Observable<boolean>((observer) => {
const proceed = () => {
this.guardLoaderIconService.hide();
observer.next(this.checkAuthAndRedirect(route, state));
};
this.authService.authChanged.subscribe({
next: () => {
this.guardLoaderIconService.hide();
observer.next(this.checkAuthAndRedirect(route, state));
next: proceed
});
/*
Fix for a race condition in admin page:
1. Page loaded,
2. Auth service loads profile, in the meantime admin page loads and checks authLoaded() which is false
3. First router waits for authChanged, authChanged is emitted, second router is loaded
4. authLoaded() still false, second router waits for authChanged but already emitted
*/
setTimeout(() => {
if(this.authService.authLoaded()) {
proceed();
}
})
}, 200);
});
}
}

View File

@ -0,0 +1,29 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin.component';
import { AuthorizeGuard } from 'src/app/_guards/authorize.guard';
const routes: Routes = [{
path: '',
component: AdminComponent,
children: [
{
path: 'info',
loadChildren: () => import('./info/admin-info.module').then(m => m.AdminInfoModule),
canActivate: [AuthorizeGuard],
data: {permissionsRequired: ['admin-read', 'admin-info-read']}
},
{
path: 'roles',
loadChildren: () => import('./roles/admin-roles.module').then(m => m.AdminRolesModule),
canActivate: [AuthorizeGuard],
data: {permissionsRequired: ['admin-read', 'admin-roles-read']}
}
]
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminRoutingModule { }

View File

@ -0,0 +1,13 @@
<div *ngIf="tabs.length > 1">
<tabset>
<tab *ngFor="let tabz of tabs" [active]="tabz.active" [heading]="tabz.title" [id]="tabz.id" (selectTab)="routeChange($event)"></tab>
</tabset>
</div>
<div *ngIf="tabs.length == 0">
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<strong>{{ 'warning'|translate|titlecase }}!</strong> {{ 'not_enough_permissions'|translate }}
</div>
</div>
<router-outlet></router-outlet>

View File

@ -0,0 +1,63 @@
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { TabDirective } from 'ngx-bootstrap/tabs';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from 'src/app/_services/auth.service';
interface ITab {
title: string;
id: string;
active: boolean;
permissionsRequired: string[];
}
@Component({
selector: 'app-admin',
templateUrl: './admin.component.html',
styleUrls: ['./admin.component.scss']
})
export class AdminComponent implements OnInit {
currRoute: string | undefined = '';
tabs: ITab[] = [
{ title: 'info', id: 'info', active: false, permissionsRequired: ['admin-read', 'admin-info-read'] },
{ title: 'roles', id: 'roles', active: false, permissionsRequired: ['admin-read', 'admin-roles-read'] }
];
constructor(
private router: Router,
private route: ActivatedRoute,
private translate: TranslateService,
private auth: AuthService
) {
// Filter out tabs that the user doesn't have permission to see
this.tabs = this.tabs.filter(t => !t.permissionsRequired.some(p => !this.auth.profile.can(p)));
// Translate tab titles
this.tabs.forEach((t) => {
this.translate.get(`menu.${t.title}`).subscribe((res: string) => {
t.title = res;
});
});
}
ngOnInit(): void {
this.currRoute = this.route?.snapshot?.firstChild?.routeConfig?.path;
if (this.currRoute) {
const tab = this.tabs.find(t => t.id === this.currRoute);
if (tab) {
tab.active = true;
}
} else if (this.tabs.length > 0) {
this.router.navigate(["/admin", this.tabs[0].id]);
this.currRoute = this.tabs[0].id;
this.tabs[0].active = true;
}
}
routeChange(data: TabDirective) {
if(!data.id || data.id == this.currRoute) return;
this.router.navigate(["/admin", data.id]);
this.currRoute = data.id;
}
}

View File

@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { TranslationModule } from '../../translation.module';
import { FirstLetterUppercasePipe } from '../../_pipes/first-letter-uppercase.pipe';
import { AdminComponent } from './admin.component';
import { AdminRoutingModule } from './admin-routing.module';
@NgModule({
declarations: [
AdminComponent
],
imports: [
CommonModule,
AdminRoutingModule,
TabsModule.forRoot(),
TranslationModule,
FirstLetterUppercasePipe
]
})
export class AdminModule { }

View File

@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminInfoComponent } from './admin-info.component';
const routes: Routes = [{ path: '', component: AdminInfoComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminInfoRoutingModule { }

View File

@ -0,0 +1 @@
Info

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-admin-info',
templateUrl: './admin-info.component.html',
styleUrls: ['./admin-info.component.scss']
})
export class AdminInfoComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,32 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { MapPickerModule } from '../../../_components/map-picker/map-picker.module';
import { DatetimePickerModule } from '../../../_components/datetime-picker/datetime-picker.module';
import { BackBtnModule } from '../../../_components/back-btn/back-btn.module';
import { TranslationModule } from '../../../translation.module';
import { FirstLetterUppercasePipe } from '../../../_pipes/first-letter-uppercase.pipe';
import { AdminInfoComponent } from './admin-info.component';
import { AdminInfoRoutingModule } from './admin-info-routing.module';
@NgModule({
declarations: [
AdminInfoComponent
],
imports: [
CommonModule,
AdminInfoRoutingModule,
FormsModule,
ReactiveFormsModule,
BsDatepickerModule.forRoot(),
MapPickerModule,
DatetimePickerModule,
BackBtnModule,
TranslationModule,
FirstLetterUppercasePipe
]
})
export class AdminInfoModule { }

View File

@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminRolesComponent } from './admin-roles.component';
const routes: Routes = [{ path: '', component: AdminRolesComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminRolesRoutingModule { }

View File

@ -0,0 +1 @@
Roles

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-admin-roles',
templateUrl: './admin-roles.component.html',
styleUrls: ['./admin-roles.component.scss']
})
export class AdminRolesComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,32 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { MapPickerModule } from '../../../_components/map-picker/map-picker.module';
import { DatetimePickerModule } from '../../../_components/datetime-picker/datetime-picker.module';
import { BackBtnModule } from '../../../_components/back-btn/back-btn.module';
import { TranslationModule } from '../../../translation.module';
import { FirstLetterUppercasePipe } from '../../../_pipes/first-letter-uppercase.pipe';
import { AdminRolesComponent } from './admin-roles.component';
import { AdminRolesRoutingModule } from './admin-roles-routing.module';
@NgModule({
declarations: [
AdminRolesComponent
],
imports: [
CommonModule,
AdminRolesRoutingModule,
FormsModule,
ReactiveFormsModule,
BsDatepickerModule.forRoot(),
MapPickerModule,
DatetimePickerModule,
BackBtnModule,
TranslationModule,
FirstLetterUppercasePipe
]
})
export class AdminRolesModule { }

View File

@ -21,7 +21,7 @@ export class AuthService {
};
public profile: any = this.defaultPlaceholderProfile;
public authChanged = new Subject<void>();
public authLoaded = false;
public _authLoaded = false;
public loadProfile() {
console.log("Loading profile data...");
@ -51,6 +51,10 @@ export class AuthService {
});
}
authLoaded() {
return this._authLoaded;
}
constructor(
private api: ApiClientService,
private authToken: AuthTokenService,
@ -61,7 +65,7 @@ export class AuthService {
}).catch(() => {
console.log("User is not logged in");
}).finally(() => {
this.authLoaded = true;
this._authLoaded = true;
});
}

View File

@ -37,10 +37,16 @@ const routes: Routes = [
data: {permissionsRequired: ['trainings-read', 'trainings-update']}
},
{
path: 'stats',
path: 'stats',
loadChildren: () => import('./_routes/stats/stats.module').then(m => m.StatsModule),
canActivate: [AuthorizeGuard]
},
{
path: 'admin',
loadChildren: () => import('./_routes/admin/admin.module').then(m => m.AdminModule),
canActivate: [AuthorizeGuard],
data: {permissionsRequired: ['admin-read']}
},
{ path: "login/:redirect/:extraParam", component: LoginComponent },
{ path: "login/:redirect", component: LoginComponent },
//

View File

@ -3,6 +3,7 @@
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/services" translate>menu.services</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/trainings" translate>menu.trainings</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/logs" translate>menu.logs</a>
<a *ngIf="auth.profile.can('admin-read')" routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/admin" translate>menu.admin</a>
<a style="float: right;" id="logout" routerLinkActive="active" [routerLink]="auth.profile.profilePageLink">{{ 'menu.hi'|translate|ftitlecase }}, {{ auth.profile.name }}. <b id="logout-text" (click)="logout($event)" translate *ngIf="!auth.profile.impersonating_user">menu.logout</b><b id="logout-text" (click)="auth.logout()" translate *ngIf="auth.profile.impersonating_user">menu.stop_impersonating</b></a>
<a class="icon" id="menuButton" (click)="menuButtonClicked = !menuButtonClicked"></a>
</div>

View File

@ -4,9 +4,12 @@
"services": "Services",
"trainings": "Trainings",
"logs": "Logs",
"admin": "Admin",
"logout": "Logout",
"stop_impersonating": "Stop impersonating",
"hi": "hi"
"hi": "hi",
"info": "Info",
"roles": "Roles"
},
"table": {
"yes_remove": "Yes, remove",

View File

@ -4,9 +4,12 @@
"services": "Interventi",
"trainings": "Esercitazioni",
"logs": "Logs",
"admin": "Amministrazione",
"logout": "Logout",
"stop_impersonating": "Torna al vero account",
"hi": "Ciao"
"hi": "Ciao",
"info": "Info",
"roles": "Ruoli"
},
"table": {
"yes_remove": "Si, rimuovi",