feat: add account approval page
MODIFY
src/app/app-routing.module.ts
src/app/app-routing.module.ts
/*
- * Copyright 2021 Scheer PAS Schweiz AG
+ * Copyright 2022 Scheer PAS Schweiz AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@ import { MarketplaceComponent } from './components/marketplace/marketplace.compo
import { AccountComponent } from './components/account/account.component';
import { MyAppsComponent } from './components/my-apps/my-apps.component';
import { AuthGuard } from './guards/auth.guard';
-import { ApprovalComponent } from './components/approval/approval.component';
import { NotificationsComponent } from './components/notifications/notifications.component';
const routes: Routes = [
@@ -50,12 +49,19 @@ const routes: Routes = [
loadChildren: () =>
import('./components/swagger/swagger.module').then((m) => m.SwaggerModule)
},
- { path: 'approval', component: ApprovalComponent },
+ {
+ path: 'approval',
+ loadChildren: () =>
+ import('./components/approval/approval.module').then(
+ (m) => m.ApprovalModule
+ )
+ },
{
path: 'notifications',
component: NotificationsComponent,
canActivate: [AuthGuard]
- }
+ },
+ { path: '**', redirectTo: '/home' }
];
@NgModule({

MODIFY
src/app/app.module.ts
src/app/app.module.ts
@@ -53,7 +53,6 @@ import { PolicyCardComponent } from './components/policy-card/policy-card.compon
import { NgxChartsModule } from '@swimlane/ngx-charts';
import { GaugeChartComponent } from './components/charts/gauge-chart/gauge-chart.component';
import { IConfig } from './interfaces/IConfig';
-import { ApprovalComponent } from './components/approval/approval.component';
import { ThemeService } from './services/theme/theme.service';
import { TocComponent } from './components/toc/toc.component';
import { PolicyCardLightComponent } from './components/policy-card-light/policy-card-light.component';
@@ -120,7 +119,6 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
PolicyCardComponent,
PolicyCardLightComponent,
GaugeChartComponent,
- ApprovalComponent,
TocComponent,
NoDataComponent,
UnregisterClientComponent,

ADD
src/app/components/approval/approval-routing.module.ts
+/*
+ * Copyright 2022 Scheer PAS Schweiz AG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ApprovalComponent } from './approval.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: ApprovalComponent,
+ children: [{ path: 'account', component: ApprovalComponent }]
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class ApprovalRoutingModule {}

MODIFY
src/app/components/approval/approval.component.html
src/app/components/approval/approval.component.html
<!--
- ~ Copyright 2021 Scheer PAS Schweiz AG
+ ~ Copyright 2022 Scheer PAS Schweiz AG
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ imitations under the License.
-->
+<div class="center mt-40" *ngIf="apiApproval; else account">
+ <h1 class="mt-40" [innerHTML]="'APPROVAL.API.TITLE' | translate"></h1>
+ <h2 [innerHTML]="'APPROVAL.API.SUBTITLE' | translate"></h2>
+ <h4 [innerHTML]="'APPROVAL.API.MORE_INFO' | translate"></h4>
+</div>
+
+<ng-template #account>
<div class="center mt-40">
- <h1 class="mt-40" [innerHTML]="'APPROVAL.TITLE' | translate"></h1>
- <h2 [innerHTML]="'APPROVAL.SUBTITLE' | translate"></h2>
- <h4 [innerHTML]="'APPROVAL.MORE_INFO' | translate"></h4>
+ <h1 class="mt-40" [innerHTML]="'APPROVAL.ACCOUNT.TITLE' | translate"></h1>
+ <h2 [innerHTML]="'APPROVAL.ACCOUNT.SUBTITLE' | translate"></h2>
+ <h4 [innerHTML]="'APPROVAL.ACCOUNT.MORE_INFO' | translate"></h4>
</div>
+</ng-template>

MODIFY
src/app/components/approval/approval.component.ts
src/app/components/approval/approval.component.ts
/*
- * Copyright 2021 Scheer PAS Schweiz AG
+ * Copyright 2022 Scheer PAS Schweiz AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
import { Component, OnInit } from '@angular/core';
import { HeroService } from '../../services/hero/hero.service';
import { TranslateService } from '@ngx-translate/core';
+import { Router } from '@angular/router';
+import { KeycloakService } from 'keycloak-angular';
@Component({
selector: 'app-approval',
@@ -24,18 +26,34 @@ import { TranslateService } from '@ngx-translate/core';
styleUrls: ['./approval.component.scss']
})
export class ApprovalComponent implements OnInit {
+ apiApproval = true;
+ private titleKey = 'APPROVAL.API.HERO_TITLE';
+
constructor(
private heroService: HeroService,
- private translator: TranslateService
+ private translator: TranslateService,
+ private router: Router,
+ private keycloak: KeycloakService
) {}
- ngOnInit(): void {
+ async ngOnInit(): Promise<void> {
+ await this.setUpApiOrAccountApprovalPage();
this.setUpHero();
}
+ private async setUpApiOrAccountApprovalPage() {
+ if (this.router.url.includes('/approval/account')) {
+ if (await this.keycloak.isLoggedIn()) {
+ await this.router.navigate(['home']);
+ }
+ this.apiApproval = false;
+ this.titleKey = 'APPROVAL.ACCOUNT.HERO_TITLE';
+ }
+ }
+
private setUpHero() {
this.heroService.setUpHero({
- title: this.translator.instant('APPROVAL.APPROVAL') as string,
+ title: this.translator.instant(this.titleKey) as string,
subtitle: ''
});
}

ADD
src/app/components/approval/approval.module.ts
+/*
+ * Copyright 2022 Scheer PAS Schweiz AG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { ApprovalRoutingModule } from './approval-routing.module';
+import { ApprovalComponent } from './approval.component';
+import { TranslateModule } from '@ngx-translate/core';
+
+@NgModule({
+ declarations: [ApprovalComponent],
+ imports: [CommonModule, ApprovalRoutingModule, TranslateModule]
+})
+export class ApprovalModule {}

MODIFY
src/app/guards/auth.guard.ts
src/app/guards/auth.guard.ts
/*
- * Copyright 2021 Scheer PAS Schweiz AG
+ * Copyright 2022 Scheer PAS Schweiz AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,11 +18,9 @@ import { Injectable } from '@angular/core';
import { Router, UrlTree } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
import { KeycloakHelperService } from '../services/keycloak-helper/keycloak-helper.service';
-import { BackendService } from '../services/backend/backend.service';
-import { ICurrentUser } from '../interfaces/ICommunication';
-import { catchError } from 'rxjs/operators';
-import { EMPTY } from 'rxjs';
import { PermissionsService } from '../services/permissions/permissions.service';
+import { ConfigService } from '../services/config/config.service';
+import { SnackbarService } from '../services/snackbar/snackbar.service';
@Injectable({
providedIn: 'root'
@@ -32,35 +30,33 @@ export class AuthGuard extends KeycloakAuthGuard {
protected readonly router: Router,
protected readonly keycloakAngular: KeycloakService,
private keycloakHelper: KeycloakHelperService,
- private backend: BackendService,
- private permissionsService: PermissionsService
+ private permissionsService: PermissionsService,
+ private config: ConfigService,
+ private snackbar: SnackbarService
) {
super(router, keycloakAngular);
}
async isAccessAllowed(): Promise<boolean | UrlTree> {
+ // Force the user to log in if currently unauthenticated
if (!this.authenticated) {
await this.keycloakAngular.login({
redirectUri: window.location.href
});
- } else {
- // we are logged in and can set the tokens
- this.keycloakHelper.setAndUpdateTokens();
+ }
- this.backend
- .getCurrentUser()
- .pipe(
- catchError((err) => {
- console.warn(err);
- this.authenticated = false;
- return EMPTY;
- })
- )
- .subscribe((user: ICurrentUser) => {
- console.log('Logged in with user: ' + user.username, user);
- this.permissionsService.setPermissions(user.permissions);
- });
+ // the user needs this roles so that backend calls will pass
+ const requiredRoles = this.config.getBackendRoles();
+ if (!requiredRoles.every((role: string) => this.roles.includes(role))) {
+ // TODO: change to error page
+ this.snackbar.showErrorSnackBar('Not enough permission');
+ return this.router.createUrlTree(['/home']);
}
- return Promise.resolve(this.authenticated);
+
+ // we are logged in and can set the tokens and fetch permissions
+ this.keycloakHelper.setAndUpdateTokens();
+ this.permissionsService.updateUserPermissions();
+
+ return this.authenticated;
}
}

MODIFY
src/app/interfaces/IConfig.ts
src/app/interfaces/IConfig.ts
/*
- * Copyright 2021 Scheer PAS Schweiz AG
+ * Copyright 2022 Scheer PAS Schweiz AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ export interface IConfig {
supportedLanguages: string[];
theme: string;
endpoint: string;
+ backendRoles: string[];
hero: IHero;
navigation: INavigation;
footer: IFooter;

MODIFY
src/app/services/backend/backend.service.ts
src/app/services/backend/backend.service.ts
/*
- * Copyright 2021 Scheer PAS Schweiz AG
+ * Copyright 2022 Scheer PAS Schweiz AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
-import { Observable } from 'rxjs';
+import { EMPTY, Observable } from 'rxjs';
import { ConfigService } from '../config/config.service';
import {
IAction,
@@ -45,6 +45,7 @@ import {
import { IPolicySummaryExt } from '../../interfaces/IPolicySummaryExt';
import { KeycloakHelperService } from '../keycloak-helper/keycloak-helper.service';
import { IPolicyProbe } from '../../interfaces/IPolicy';
+import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
@@ -385,9 +386,13 @@ export class BackendService {
public getCurrentUser(): Observable<ICurrentUser> {
const path = 'users/currentuser/info';
- return this.http.get<ICurrentUser>(
- this.generateUrl(path),
- this.httpOptions
+ return this.http
+ .get<ICurrentUser>(this.generateUrl(path), this.httpOptions)
+ .pipe(
+ catchError((err) => {
+ console.error(err);
+ return EMPTY;
+ })
);
}

MODIFY
src/app/services/config/config.service.ts
src/app/services/config/config.service.ts
@@ -77,6 +77,10 @@ export class ConfigService {
return this.config.endpoint;
}
+ getBackendRoles(): string[] {
+ return this.config.backendRoles;
+ }
+
getAuth(): IAuthProvider {
return { ...this.config.auth };
}

MODIFY
src/app/services/keycloak-helper/keycloak-helper.service.ts
src/app/services/keycloak-helper/keycloak-helper.service.ts
/*
- * Copyright 2021 Scheer PAS Schweiz AG
+ * Copyright 2022 Scheer PAS Schweiz AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -43,7 +43,8 @@ export class KeycloakHelperService {
* Init keycloak setting, this is called via APP Initializer
*/
public initKeycloak(): Promise<boolean> {
- return this.keycloak.init({
+ return this.keycloak
+ .init({
config: {
url: this.configService.getAuth().url,
realm: this.configService.getAuth().realm,
@@ -58,6 +59,16 @@ export class KeycloakHelperService {
loadUserProfileAtStartUp: true, // because of https://github.com/mauriciovigolo/keycloak-angular/pull/269
enableBearerInterceptor: true,
bearerExcludedUrls: ['/assets']
+ })
+ .catch((error) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ if (error.error && error.error === 'access_denied') {
+ // if we got an access_denied we still can init the keycloak library
+ return true;
+ } else {
+ console.error(error);
+ throw error;
+ }
});
}

MODIFY
src/app/services/permissions/permissions.service.ts
src/app/services/permissions/permissions.service.ts
/*
- * Copyright 2021 Scheer PAS Schweiz AG
+ * Copyright 2022 Scheer PAS Schweiz AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*/
import { Injectable } from '@angular/core';
-import { IPermission } from '../../interfaces/ICommunication';
+import { ICurrentUser, IPermission } from '../../interfaces/ICommunication';
+import { BackendService } from '../backend/backend.service';
@Injectable({
providedIn: 'root'
@@ -23,9 +24,16 @@ import { IPermission } from '../../interfaces/ICommunication';
export class PermissionsService {
private permissions: IPermission[] = [];
- constructor() {}
+ constructor(private backend: BackendService) {}
- public setPermissions(permissions: IPermission[]): void {
+ public updateUserPermissions(): void {
+ this.backend.getCurrentUser().subscribe((user: ICurrentUser) => {
+ console.log('Logged in as user: ' + user.username, user);
+ this.setPermissions(user.permissions);
+ });
+ }
+
+ private setPermissions(permissions: IPermission[]): void {
this.permissions = permissions;
}

MODIFY
src/assets/config.json5
src/assets/config.json5
},
// specify the entrypoint to the API Management Manager: This is MANDATORY for communication with backend
"endpoint": "https://vagrantguest/pas/apiman",
+ // the SSO role that the manager endpoint requires
+ "backendRoles": ["view-profile", "apiuser"],
// set authentication details when pressing "login" telling where to redirect for SSO login with keycloak
"auth": {
"url": "https://vagrantguest/pas/keycloak",

MODIFY
src/assets/i18n/en.json
src/assets/i18n/en.json
"API_ENDPOINT": "One important aspect of all APIs that are managed by the API Gateway is the security that the API Gateway provides. <br>The API can be accessed with this URL."
},
"APPROVAL": {
- "APPROVAL": "Approval Required",
- "TITLE": "Thank you.",
+ "API": {
+ "HERO_TITLE": "Approval Required",
+ "TITLE": "Thank you",
"SUBTITLE": "Your subscription is awaiting approval",
"MORE_INFO": "You will receive an email notification when there is progress in your application"
},
+ "ACCOUNT": {
+ "HERO_TITLE": "Approval Required",
+ "TITLE": "Thank you",
+ "SUBTITLE": "Your account is awaiting approval",
+ "MORE_INFO": "You will receive an email notification when there is progress"
+ }
+ },
"DIALOGS": {
"REMOVE_TITLE": "Remove Application",
"REMOVE_SURE": "Are you sure you want to <span class='warn'>remove</span> the application <b>{{value}}</b> and all of its contracts?",

feat: add account approval page