diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts
index b8fe0354e..a99349460 100644
--- a/apps/red-ui/src/app/app.module.ts
+++ b/apps/red-ui/src/app/app.module.ts
@@ -1,58 +1,60 @@
-import { BrowserModule } from '@angular/platform-browser';
-import { APP_INITIALIZER, NgModule } from '@angular/core';
+import {BrowserModule} from '@angular/platform-browser';
+import {APP_INITIALIZER, NgModule} from '@angular/core';
-import { AppComponent } from './app.component';
-import {ActivatedRoute, ActivatedRouteSnapshot, Router, RouterModule} from '@angular/router';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-import { ReactiveFormsModule, FormsModule } from '@angular/forms';
-import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http';
-import { BaseScreenComponent } from './screens/base-screen/base-screen.component';
-import { ProjectListingScreenComponent } from './screens/project-listing-screen/project-listing-screen.component';
-import { ProjectOverviewScreenComponent } from './screens/project-overview-screen/project-overview-screen.component';
-import { MatToolbarModule } from '@angular/material/toolbar';
-import { ApiModule } from '@redaction/red-ui-http';
-import { ApiPathInterceptorService } from './interceptor/api-path-interceptor.service';
-import { MatButtonModule } from '@angular/material/button';
-import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
-import { TranslateHttpLoader } from '@ngx-translate/http-loader';
-import { MatMenuModule } from '@angular/material/menu';
-import { languageInitializer } from './i18n/language.initializer';
-import { LanguageService } from './i18n/language.service';
-import { MatIconModule } from '@angular/material/icon';
-import { IconsModule } from './icons/icons.module';
-import { AddEditProjectDialogComponent } from './screens/project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component';
-import { MatDialogModule } from '@angular/material/dialog';
-import { MatSnackBarModule } from '@angular/material/snack-bar';
-import { MatTooltipModule } from '@angular/material/tooltip';
-import { ConfirmationDialogComponent } from './common/confirmation-dialog/confirmation-dialog.component';
-import { FilePreviewScreenComponent } from './screens/file/file-preview-screen/file-preview-screen.component';
-import { PdfViewerComponent } from './screens/file/pdf-viewer/pdf-viewer.component';
-import { MatTabsModule } from '@angular/material/tabs';
-import { MatButtonToggleModule } from '@angular/material/button-toggle';
-import { NgpSortModule } from 'ngp-sort-pipe';
-import { MatFormFieldModule } from '@angular/material/form-field';
-import { MatSelectModule } from '@angular/material/select';
-import { MatSidenavModule } from '@angular/material/sidenav';
-import { FileDetailsDialogComponent } from './screens/file/file-preview-screen/file-details-dialog/file-details-dialog.component';
-import { ToastrModule } from 'ngx-toastr';
-import { ServiceWorkerModule } from '@angular/service-worker';
-import { environment } from '../environments/environment';
-import { ProjectDetailsDialogComponent } from './screens/project-overview-screen/project-details-dialog/project-details-dialog.component';
-import { AuthModule } from './auth/auth.module';
-import { FileUploadModule } from './upload/file-upload.module';
-import { FullPageLoadingIndicatorComponent } from './utils/full-page-loading-indicator/full-page-loading-indicator.component';
-import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
-import { InitialsAvatarComponent } from './common/initials-avatar/initials-avatar.component';
-import { StatusBarComponent } from './components/status-bar/status-bar.component';
-import { LogoComponent } from './logo/logo.component';
-import { CompositeRouteGuard } from './utils/composite-route.guard';
-import { AppStateGuard } from './state/app-state.guard';
-import { SimpleDoughnutChartComponent } from './components/simple-doughnut-chart/simple-doughnut-chart.component';
-import { ManualRedactionDialogComponent } from './screens/file/manual-redaction-dialog/manual-redaction-dialog.component';
-import { MatCheckboxModule } from '@angular/material/checkbox';
-import { MatSlideToggleModule } from '@angular/material/slide-toggle';
-import { AnnotationIconComponent } from './components/annotation-icon/annotation-icon.component';
-import { AuthGuard } from "./auth/auth.guard";
+import {AppComponent} from './app.component';
+import {ActivatedRoute, Router, RouterModule} from '@angular/router';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from '@angular/common/http';
+import {BaseScreenComponent} from './screens/base-screen/base-screen.component';
+import {ProjectListingScreenComponent} from './screens/project-listing-screen/project-listing-screen.component';
+import {ProjectOverviewScreenComponent} from './screens/project-overview-screen/project-overview-screen.component';
+import {MatToolbarModule} from '@angular/material/toolbar';
+import {ApiModule} from '@redaction/red-ui-http';
+import {ApiPathInterceptorService} from './interceptor/api-path-interceptor.service';
+import {MatButtonModule} from '@angular/material/button';
+import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
+import {TranslateHttpLoader} from '@ngx-translate/http-loader';
+import {MatMenuModule} from '@angular/material/menu';
+import {languageInitializer} from './i18n/language.initializer';
+import {LanguageService} from './i18n/language.service';
+import {MatIconModule} from '@angular/material/icon';
+import {IconsModule} from './icons/icons.module';
+import {AddEditProjectDialogComponent} from './screens/project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component';
+import {MatDialogModule} from '@angular/material/dialog';
+import {MatSnackBarModule} from '@angular/material/snack-bar';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {ConfirmationDialogComponent} from './common/confirmation-dialog/confirmation-dialog.component';
+import {FilePreviewScreenComponent} from './screens/file/file-preview-screen/file-preview-screen.component';
+import {PdfViewerComponent} from './screens/file/pdf-viewer/pdf-viewer.component';
+import {MatTabsModule} from '@angular/material/tabs';
+import {MatButtonToggleModule} from '@angular/material/button-toggle';
+import {NgpSortModule} from 'ngp-sort-pipe';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatSelectModule} from '@angular/material/select';
+import {MatSidenavModule} from '@angular/material/sidenav';
+import {FileDetailsDialogComponent} from './screens/file/file-preview-screen/file-details-dialog/file-details-dialog.component';
+import {ToastrModule} from 'ngx-toastr';
+import {ServiceWorkerModule} from '@angular/service-worker';
+import {environment} from '../environments/environment';
+import {ProjectDetailsDialogComponent} from './screens/project-overview-screen/project-details-dialog/project-details-dialog.component';
+import {AuthModule} from './auth/auth.module';
+import {FileUploadModule} from './upload/file-upload.module';
+import {FullPageLoadingIndicatorComponent} from './utils/full-page-loading-indicator/full-page-loading-indicator.component';
+import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
+import {InitialsAvatarComponent} from './common/initials-avatar/initials-avatar.component';
+import {StatusBarComponent} from './components/status-bar/status-bar.component';
+import {LogoComponent} from './logo/logo.component';
+import {CompositeRouteGuard} from './utils/composite-route.guard';
+import {AppStateGuard} from './state/app-state.guard';
+import {SimpleDoughnutChartComponent} from './components/simple-doughnut-chart/simple-doughnut-chart.component';
+import {ManualRedactionDialogComponent} from './screens/file/manual-redaction-dialog/manual-redaction-dialog.component';
+import {MatCheckboxModule} from '@angular/material/checkbox';
+import {MatSlideToggleModule} from '@angular/material/slide-toggle';
+import {AnnotationIconComponent} from './components/annotation-icon/annotation-icon.component';
+import {AuthGuard} from "./auth/auth.guard";
+import {AuthErrorComponent} from './screens/auth-error/auth-error.component';
+import {RedRoleGuard} from "./auth/red-role.guard";
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
@@ -77,6 +79,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
SimpleDoughnutChartComponent,
ManualRedactionDialogComponent,
AnnotationIconComponent,
+ AuthErrorComponent,
],
imports: [
BrowserModule,
@@ -101,6 +104,11 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
redirectTo: 'ui/projects',
pathMatch: 'full'
},
+ {
+ path: 'auth-error',
+ component: AuthErrorComponent,
+ canActivate: [AuthGuard]
+ },
{
path: 'ui',
component: BaseScreenComponent,
@@ -110,7 +118,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
component: ProjectListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
- routeGuards: [AuthGuard, AppStateGuard]
+ routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
@@ -118,7 +126,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
component: ProjectOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
- routeGuards: [AuthGuard, AppStateGuard]
+ routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
@@ -126,7 +134,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
component: FilePreviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
- routeGuards: [AuthGuard, AppStateGuard]
+ routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
}
]
@@ -148,7 +156,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
MatSelectModule,
MatSidenavModule,
FileUploadModule,
- ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
+ ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}),
MatProgressSpinnerModule,
MatCheckboxModule
],
@@ -166,9 +174,9 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
})
export class AppModule {
- constructor(private router: Router,private route: ActivatedRoute) {
- route.queryParamMap.subscribe(queryParams=>{
- if(queryParams.has('code') || queryParams.has('state') || queryParams.has('session_state')) {
+ constructor(private router: Router, private route: ActivatedRoute) {
+ route.queryParamMap.subscribe(queryParams => {
+ if (queryParams.has('code') || queryParams.has('state') || queryParams.has('session_state')) {
this.router.navigate([], {
queryParams: {
'state': null,
diff --git a/apps/red-ui/src/app/auth/auth.guard.ts b/apps/red-ui/src/app/auth/auth.guard.ts
index b01f1d182..3e5d8f4ea 100644
--- a/apps/red-ui/src/app/auth/auth.guard.ts
+++ b/apps/red-ui/src/app/auth/auth.guard.ts
@@ -2,6 +2,7 @@ import {Injectable} from "@angular/core";
import {ActivatedRouteSnapshot, Router, RouterStateSnapshot} from "@angular/router";
import {KeycloakAuthGuard, KeycloakService} from "keycloak-angular";
import {UserService} from "../user/user.service";
+import {AppLoadStateService} from "../utils/app-load-state.service";
@Injectable({
providedIn: 'root',
@@ -10,6 +11,7 @@ export class AuthGuard extends KeycloakAuthGuard {
constructor(
protected readonly _router: Router,
protected readonly _keycloak: KeycloakService,
+ private readonly _appLoadStateService: AppLoadStateService,
private readonly _userService: UserService
) {
super(_router, _keycloak);
@@ -25,6 +27,7 @@ export class AuthGuard extends KeycloakAuthGuard {
await this._userService.loadCurrentUser();
+
return true;
}
}
diff --git a/apps/red-ui/src/app/auth/red-role.guard.ts b/apps/red-ui/src/app/auth/red-role.guard.ts
new file mode 100644
index 000000000..6576473a8
--- /dev/null
+++ b/apps/red-ui/src/app/auth/red-role.guard.ts
@@ -0,0 +1,30 @@
+import {Injectable} from "@angular/core";
+import {ActivatedRouteSnapshot, Router, RouterStateSnapshot} from "@angular/router";
+import {KeycloakAuthGuard, KeycloakService} from "keycloak-angular";
+import {UserService} from "../user/user.service";
+import {AppLoadStateService} from "../utils/app-load-state.service";
+
+@Injectable({
+ providedIn: 'root',
+})
+export class RedRoleGuard extends KeycloakAuthGuard {
+ constructor(
+ protected readonly _router: Router,
+ protected readonly _keycloak: KeycloakService,
+ private readonly _appLoadStateService: AppLoadStateService,
+ private readonly _userService: UserService
+ ) {
+ super(_router, _keycloak);
+ }
+
+ public async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+
+ if (!this._userService.user.hasAnyREDRoles) {
+ this._router.navigate(['/auth-error']);
+ this._appLoadStateService.pushLoadingEvent(false);
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/apps/red-ui/src/app/screens/auth-error/auth-error.component.html b/apps/red-ui/src/app/screens/auth-error/auth-error.component.html
new file mode 100644
index 000000000..d0dd895e7
--- /dev/null
+++ b/apps/red-ui/src/app/screens/auth-error/auth-error.component.html
@@ -0,0 +1,4 @@
+
diff --git a/apps/red-ui/src/app/screens/auth-error/auth-error.component.scss b/apps/red-ui/src/app/screens/auth-error/auth-error.component.scss
new file mode 100644
index 000000000..500a55a0d
--- /dev/null
+++ b/apps/red-ui/src/app/screens/auth-error/auth-error.component.scss
@@ -0,0 +1,3 @@
+section {
+ padding: 24px;
+}
diff --git a/apps/red-ui/src/app/screens/auth-error/auth-error.component.ts b/apps/red-ui/src/app/screens/auth-error/auth-error.component.ts
new file mode 100644
index 000000000..b523af973
--- /dev/null
+++ b/apps/red-ui/src/app/screens/auth-error/auth-error.component.ts
@@ -0,0 +1,19 @@
+import { Component, OnInit } from '@angular/core';
+import {UserService} from "../../user/user.service";
+
+@Component({
+ selector: 'redaction-auth-error',
+ templateUrl: './auth-error.component.html',
+ styleUrls: ['./auth-error.component.scss']
+})
+export class AuthErrorComponent implements OnInit {
+
+ constructor(private readonly _userService: UserService) { }
+
+ ngOnInit(): void {
+ }
+
+ logout() {
+ this._userService.logout();
+ }
+}
diff --git a/apps/red-ui/src/app/user/user.service.ts b/apps/red-ui/src/app/user/user.service.ts
index f6f99dcde..d4e9e59fa 100644
--- a/apps/red-ui/src/app/user/user.service.ts
+++ b/apps/red-ui/src/app/user/user.service.ts
@@ -44,7 +44,7 @@ export class UserService {
}
logout() {
- this._keycloakService.logout();
+ this._keycloakService.logout(window.location.origin);
}
async loadCurrentUser() {
diff --git a/apps/red-ui/src/app/utils/composite-route.guard.ts b/apps/red-ui/src/app/utils/composite-route.guard.ts
index 6a62a009d..f37ddefcd 100644
--- a/apps/red-ui/src/app/utils/composite-route.guard.ts
+++ b/apps/red-ui/src/app/utils/composite-route.guard.ts
@@ -1,7 +1,7 @@
-import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
-import { Injectable, Injector } from '@angular/core';
-import { Observable, of } from 'rxjs';
-import { catchError, mergeMap, tap } from 'rxjs/operators';
+import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router';
+import {Injectable, Injector} from '@angular/core';
+import {Observable, of} from 'rxjs';
+import {catchError, mergeMap, tap} from 'rxjs/operators';
import {AppLoadStateService} from "./app-load-state.service";
@Injectable({
@@ -10,40 +10,41 @@ import {AppLoadStateService} from "./app-load-state.service";
export class CompositeRouteGuard implements CanActivate {
- constructor(protected router: Router, protected injector: Injector, private appLoadStateService: AppLoadStateService) {}
+ constructor(protected readonly _router: Router, protected readonly _injector: Injector, private readonly _appLoadStateService: AppLoadStateService) {
+ }
- canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
- this.appLoadStateService.pushLoadingEvent(true);
- let compositeCanActivateObservable: Observable = of(true);
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ this._appLoadStateService.pushLoadingEvent(true);
+ let compositeCanActivateObservable: Observable = of(true);
- const routeGuards = route.data.routeGuards;
-
- if (routeGuards) {
- for (let i = 0; i < routeGuards.length; i++) {
- const routeGuard = this.injector.get(routeGuards[i]);
- const canActivateObservable: Observable = routeGuard.canActivate(route, state);
- compositeCanActivateObservable = compositeCanActivateObservable.pipe(
- mergeMap(bool => {
- if (!bool) {
- return of(false);
- } else {
- return canActivateObservable;
- }
- })
- );
- }
- }
+ const routeGuards = route.data.routeGuards;
+ if (routeGuards) {
+ for (let i = 0; i < routeGuards.length; i++) {
+ const routeGuard = this._injector.get(routeGuards[i]);
+ const canActivateObservable: Observable = routeGuard.canActivate(route, state);
compositeCanActivateObservable = compositeCanActivateObservable.pipe(
- tap(() => {
- this.appLoadStateService.pushLoadingEvent(false);
- }),
- catchError(() => {
- this.appLoadStateService.pushLoadingEvent(false);
- return of(false);
- })
+ mergeMap(bool => {
+ if (!bool) {
+ return of(false);
+ } else {
+ return canActivateObservable;
+ }
+ })
);
-
- return compositeCanActivateObservable;
+ }
}
+
+ compositeCanActivateObservable = compositeCanActivateObservable.pipe(
+ tap(() => {
+ this._appLoadStateService.pushLoadingEvent(false);
+ }),
+ catchError(() => {
+ this._appLoadStateService.pushLoadingEvent(false);
+ return of(false);
+ })
+ );
+
+ return compositeCanActivateObservable;
+ }
}
diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json
index bc41964a8..7e0d34b4c 100644
--- a/apps/red-ui/src/assets/i18n/en.json
+++ b/apps/red-ui/src/assets/i18n/en.json
@@ -1,4 +1,12 @@
{
+ "auth-error": {
+ "heading": {
+ "label": "Your user doesn't have the required RED-* roles to access this application"
+ },
+ "logout": {
+ "label": "Logout"
+ }
+ },
"manual-redaction": {
"remove-annotation": {
"success": {