Add admin page and fix race condition in AuthorizeGuard
This commit is contained in:
parent
350e0b22bd
commit
5b109da648
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { }
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 { }
|
|
@ -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 { }
|
|
@ -0,0 +1 @@
|
|||
Info
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
|
@ -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 { }
|
|
@ -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 { }
|
|
@ -0,0 +1 @@
|
|||
Roles
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
|
@ -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 { }
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 },
|
||||
//
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue