diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c9dae5e..50d90e32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ This project tries to follow [SemVer 2.0.0](https://semver.org/).
## v1.6.0 (WIP)
+- Adds the ability to login. While logged in this will forward the OIDC
+ access token to the backend such that a secure user access control is
+ established. (#70)
+
- Changed function calls and type names to match the regenerated rest clients
using: (#91)
diff --git a/angular.json b/angular.json
index f8f9f335..a8499088 100644
--- a/angular.json
+++ b/angular.json
@@ -71,7 +71,8 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
- "browserTarget": "wharf:build"
+ "browserTarget": "wharf:build",
+ "proxyConfig": "src/proxy/docker.dev.conf.json"
},
"configurations": {
"production": {
diff --git a/package-lock.json b/package-lock.json
index bd319361..b0818fc0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -724,18 +724,102 @@
}
},
"@angular-eslint/schematics": {
- "version": "12.2.0",
- "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-12.2.0.tgz",
- "integrity": "sha512-HsUjfjvGzCv5zygwcxPP0zPp/qTCp4JZlA+8m59S1X5w+yy2zt/vQW/8bOFUtVJCFTa+KcxDt/fzU/qSZKpCGw==",
+ "version": "12.3.1",
+ "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-12.3.1.tgz",
+ "integrity": "sha512-r1yZaqyO0oJhKDIWio3gH9TWpWEN8bUpiftgkkR6Yyc4hKBbiR5N4RQo5aZ5bnGytdW8QP8zq2k1yYBWjhEX7A==",
"dev": true,
"requires": {
- "@angular-eslint/eslint-plugin": "12.2.0",
- "@angular-eslint/eslint-plugin-template": "12.2.0",
+ "@angular-eslint/eslint-plugin": "12.3.1",
+ "@angular-eslint/eslint-plugin-template": "12.3.1",
"ignore": "5.1.8",
"strip-json-comments": "3.1.1",
"tmp": "0.2.1"
},
"dependencies": {
+ "@angular-eslint/eslint-plugin": {
+ "version": "12.3.1",
+ "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-12.3.1.tgz",
+ "integrity": "sha512-KBm27onYggRcusA/BxuSkDGpVnIs8yG4ARio8ZAhe0H2XIRJTzJZ7oIBBjugDau03AGX3VMG6wAXailjJvsywg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/experimental-utils": "4.28.2"
+ }
+ },
+ "@angular-eslint/eslint-plugin-template": {
+ "version": "12.3.1",
+ "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-12.3.1.tgz",
+ "integrity": "sha512-pz+nO64ma/9Sp2aeRnQ+Vktt7Fo1Lay/J+CG//3TIc3lYsoCTj4h42P6yCcxxJ9b4N7SUxMAnchA8eE5mJS4Ug==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/experimental-utils": "4.28.2",
+ "aria-query": "^4.2.2",
+ "axobject-query": "^2.2.0"
+ }
+ },
+ "@typescript-eslint/experimental-utils": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.2.tgz",
+ "integrity": "sha512-MwHPsL6qo98RC55IoWWP8/opTykjTp4JzfPu1VfO2Z0MshNP0UZ1GEV5rYSSnZSUI8VD7iHvtIPVGW5Nfh7klQ==",
+ "dev": true,
+ "requires": {
+ "@types/json-schema": "^7.0.7",
+ "@typescript-eslint/scope-manager": "4.28.2",
+ "@typescript-eslint/types": "4.28.2",
+ "@typescript-eslint/typescript-estree": "4.28.2",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ }
+ },
+ "@typescript-eslint/scope-manager": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.2.tgz",
+ "integrity": "sha512-MqbypNjIkJFEFuOwPWNDjq0nqXAKZvDNNs9yNseoGBB1wYfz1G0WHC2AVOy4XD7di3KCcW3+nhZyN6zruqmp2A==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.28.2",
+ "@typescript-eslint/visitor-keys": "4.28.2"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.2.tgz",
+ "integrity": "sha512-Gr15fuQVd93uD9zzxbApz3wf7ua3yk4ZujABZlZhaxxKY8ojo448u7XTm/+ETpy0V0dlMtj6t4VdDvdc0JmUhA==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.2.tgz",
+ "integrity": "sha512-86lLstLvK6QjNZjMoYUBMMsULFw0hPHJlk1fzhAVoNjDBuPVxiwvGuPQq3fsBMCxuDJwmX87tM/AXoadhHRljg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.28.2",
+ "@typescript-eslint/visitor-keys": "4.28.2",
+ "debug": "^4.3.1",
+ "globby": "^11.0.3",
+ "is-glob": "^4.0.1",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.2.tgz",
+ "integrity": "sha512-aT2B4PLyyRDUVUafXzpZFoc0C9t0za4BJAKP5sgWIhG+jHECQZUEjuQSCIwZdiJJ4w4cgu5r3Kh20SOdtEBl0w==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.28.2",
+ "eslint-visitor-keys": "^2.0.0"
+ }
+ },
+ "eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^2.0.0"
+ }
+ },
"tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
@@ -4807,6 +4891,15 @@
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
"dev": true
},
+ "angular-auth-oidc-client": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/angular-auth-oidc-client/-/angular-auth-oidc-client-13.1.0.tgz",
+ "integrity": "sha512-52RgjL0ScoRdM+QM9bYW7Qd0NMjK8GfxHPsqyVJw4KLcpyIdOT6SW3ThQ63oFvxPAGmvFBmXhdB0AqCab9xVuA==",
+ "requires": {
+ "rfc4648": "^1.5.0",
+ "tslib": "^2.3.0"
+ }
+ },
"ansi-colors": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
@@ -16161,6 +16254,11 @@
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true
},
+ "rfc4648": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.1.tgz",
+ "integrity": "sha512-60e/YWs2/D3MV1ErdjhJHcmlgnyLUiG4X/14dgsfm9/zmCWLN16xI6YqJYSCd/OANM7bUNzJqPY5B8/02S9Ibw=="
+ },
"rfdc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
diff --git a/package.json b/package.json
index 3fed2d5c..cf038e91 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
+ "start-local": "ng serve --proxy-config src/proxy/local.dev.conf.json",
"build": "ng build",
"build-prod": "node deploy/collect-licenses/collect-licenses.mjs && ng build -c production",
"build-clients": "ng build api-client && ng build import-gitlab-client && ng build import-github-client && ng build import-azuredevops-client",
@@ -28,10 +29,12 @@
"@angular/platform-browser-dynamic": "^12.1.1",
"@angular/router": "^12.1.1",
"@fortawesome/fontawesome-free": "^5.15.3",
+ "angular-auth-oidc-client": "^13.1.0",
"ng-event-source": "^1.0.14",
"primeicons": "^4.1.0",
"primeng": "^12.0.0",
"prismjs": "^1.27.0",
+ "rfc4648": "^1.5.1",
"rxjs": "^7.2.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
@@ -42,7 +45,7 @@
"@angular-eslint/builder": "12.2.0",
"@angular-eslint/eslint-plugin": "12.2.0",
"@angular-eslint/eslint-plugin-template": "12.2.0",
- "@angular-eslint/schematics": "12.2.0",
+ "@angular-eslint/schematics": "12.3.1",
"@angular-eslint/template-parser": "12.2.0",
"@angular/cli": "^12.1.1",
"@angular/compiler-cli": "^12.1.1",
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 69bbb332..98f26057 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -4,6 +4,9 @@ import { ProjectListComponent } from './projects/project-list/project-list.compo
import { ProjectDetailsComponent } from './projects/project-details/project-details.component';
import { BuildDetailsComponent } from './builds/build-details/build-details.component';
import { LicensesComponent } from './licenses/licenses.component';
+import { LoginComponent } from './auth/login/login.component';
+import { UnauthorizedComponent } from './auth/unauthorized/unauthorized.component';
+import { ForbiddenComponent } from './auth/forbidden/forbidden.component';
const routes: Routes = [
@@ -11,6 +14,9 @@ const routes: Routes = [
{ path: 'project/:projectId', component: ProjectDetailsComponent },
{ path: 'build/:projectId/:buildId', component: BuildDetailsComponent },
{ path: 'third-party-licenses', component: LicensesComponent },
+ { path: 'login', component: LoginComponent },
+ { path: 'unauthorized', component: UnauthorizedComponent},
+ { path: 'forbidden', component: ForbiddenComponent},
];
@NgModule({
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 813a935a..24c7a125 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,10 +1,24 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
+import { OidcSecurityService } from 'angular-auth-oidc-client';
@Component({
selector: 'wh-app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
-export class AppComponent {
+export class AppComponent implements OnInit{
title = 'wharf';
+
+ constructor(
+ private oidcSecurityService: OidcSecurityService,
+ ) {
+ }
+
+ ngOnInit() {
+ // The method checkAuth() is needed to process the redirect from your Security Token Service and set the
+ // correct states. This method must be used to ensure the correct functioning of the library.
+ this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData, accessToken, idToken }) => {
+ console.warn('Authenticated: ', isAuthenticated);
+ });
+ }
}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 14b175fc..b83bb6a3 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -16,6 +16,7 @@ import { SyntaxHighlightService } from './shared/syntax-highlight/syntax-highlig
import { SharedModule } from './shared/pipes/shared.module';
import { NavModule } from './nav/nav.module';
import { LicensesModule } from './licenses/licenses.module';
+import { AuthModule } from './auth/auth.module';
@NgModule({
declarations: [
@@ -34,6 +35,7 @@ import { LicensesModule } from './licenses/licenses.module';
MenuModule,
TooltipModule,
SharedModule,
+ AuthModule,
],
providers: [
{
diff --git a/src/app/auth/auth-config.module.ts b/src/app/auth/auth-config.module.ts
new file mode 100644
index 00000000..a6e1b134
--- /dev/null
+++ b/src/app/auth/auth-config.module.ts
@@ -0,0 +1,29 @@
+import { NgModule } from '@angular/core';
+import {
+ AuthModule, OidcSecurityService,
+ StsConfigHttpLoader,
+ StsConfigLoader,
+} from 'angular-auth-oidc-client';
+import { HTTP_INTERCEPTORS } from '@angular/common/http';
+import { WharfAuthInterceptor } from './wharf-auth.interceptor';
+import { ConfigService } from '../shared/config/config.service';
+
+const authFactory = (configService: ConfigService) => new StsConfigHttpLoader(configService.getOidcConfig$());
+
+@NgModule({
+ imports: [
+ AuthModule.forRoot({
+ loader: {
+ provide: StsConfigLoader,
+ useFactory: authFactory,
+ deps: [ConfigService],
+ },
+ }),
+ ],
+ exports: [AuthModule],
+ declarations: [],
+ providers: [
+ {provide: HTTP_INTERCEPTORS, useClass: WharfAuthInterceptor, multi: true},
+ ],
+})
+export class AuthConfigModule {}
diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts
new file mode 100644
index 00000000..77bb76ee
--- /dev/null
+++ b/src/app/auth/auth.module.ts
@@ -0,0 +1,40 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { RouterModule } from '@angular/router';
+import { filter } from 'rxjs/operators';
+import { DialogModule } from 'primeng/dialog';
+import { ButtonModule } from 'primeng/button';
+import { EventTypes, OidcSecurityService, PublicEventsService } from 'angular-auth-oidc-client';
+import { AuthConfigModule } from './auth-config.module';
+import { ForbiddenComponent } from './forbidden/forbidden.component';
+import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
+import { LoginComponent } from './login/login.component';
+import { CardModule } from 'primeng/card';
+
+@NgModule({
+ declarations: [
+ ForbiddenComponent,
+ UnauthorizedComponent,
+ LoginComponent,
+ ],
+ imports: [
+ CommonModule,
+ AuthConfigModule,
+ DialogModule,
+ RouterModule,
+ ButtonModule,
+ CardModule,
+ ],
+})
+export class AuthModule {
+ constructor(
+ private readonly eventService: PublicEventsService,
+ ) {
+ this.eventService
+ .registerForEvents()
+ .pipe(filter((notification) => notification.type === EventTypes.ConfigLoaded))
+ .subscribe((config) => {
+ console.log('ConfigLoaded', config);
+ });
+ }
+}
diff --git a/src/app/auth/forbidden/forbidden.component.html b/src/app/auth/forbidden/forbidden.component.html
new file mode 100644
index 00000000..1ef377b1
--- /dev/null
+++ b/src/app/auth/forbidden/forbidden.component.html
@@ -0,0 +1,4 @@
+
diff --git a/src/app/auth/forbidden/forbidden.component.scss b/src/app/auth/forbidden/forbidden.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/auth/forbidden/forbidden.component.spec.ts b/src/app/auth/forbidden/forbidden.component.spec.ts
new file mode 100644
index 00000000..29926e8a
--- /dev/null
+++ b/src/app/auth/forbidden/forbidden.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ForbiddenComponent } from './forbidden.component';
+
+describe('ForbiddenComponent', () => {
+ let component: ForbiddenComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ ForbiddenComponent ],
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ForbiddenComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/auth/forbidden/forbidden.component.ts b/src/app/auth/forbidden/forbidden.component.ts
new file mode 100644
index 00000000..16a733be
--- /dev/null
+++ b/src/app/auth/forbidden/forbidden.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'wh-forbidden',
+ templateUrl: './forbidden.component.html',
+ styleUrls: ['./forbidden.component.scss'],
+})
+export class ForbiddenComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/src/app/auth/login/login.component.html b/src/app/auth/login/login.component.html
new file mode 100644
index 00000000..250b2a5f
--- /dev/null
+++ b/src/app/auth/login/login.component.html
@@ -0,0 +1,23 @@
+
+
+
Logout
+
Logout and revoke tokens
+
Revoke access token
+
Revoke refresh token
+
Refresh session
+
+
+ Is Authenticated: {{ isAuthenticated$|async }}
+
+ userData
+
{{ userData$ | async | json }}
+
+
+
+
+ Login
+
+
+
+Configuration loaded:
+{{ configuration | json }}
diff --git a/src/app/auth/login/login.component.scss b/src/app/auth/login/login.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/auth/login/login.component.spec.ts b/src/app/auth/login/login.component.spec.ts
new file mode 100644
index 00000000..ea7fc891
--- /dev/null
+++ b/src/app/auth/login/login.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LoginComponent } from './login.component';
+
+describe('LoginModalComponent', () => {
+ let component: LoginComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ LoginComponent ],
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LoginComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/auth/login/login.component.ts b/src/app/auth/login/login.component.ts
new file mode 100644
index 00000000..a8d9814c
--- /dev/null
+++ b/src/app/auth/login/login.component.ts
@@ -0,0 +1,59 @@
+import { Component, OnInit } from '@angular/core';
+import {
+ AuthenticatedResult,
+ OidcSecurityService,
+ OpenIdConfiguration,
+ UserDataResult,
+} from 'angular-auth-oidc-client';
+import { Observable, pluck } from 'rxjs';
+
+/*
+ * Largely from damienbod/angular-auth-oidc-client samples. See-
+ * https://github.com/damienbod/angular-auth-oidc-client/tree/main/projects/sample-code-flow-refresh-tokens/src/app
+ */
+@Component({
+ selector: 'wh-login',
+ templateUrl: './login.component.html',
+ styleUrls: ['./login.component.scss'],
+})
+export class LoginComponent implements OnInit {
+
+ configuration: OpenIdConfiguration;
+ userData$: Observable;
+ isAuthenticated$: Observable;
+
+ constructor(
+ public oidcSecurityService: OidcSecurityService,
+ ) { }
+
+ ngOnInit() {
+ this.configuration = this.oidcSecurityService.getConfiguration();
+ this.userData$ = this.oidcSecurityService.userData$;
+ this.isAuthenticated$ = this.oidcSecurityService.isAuthenticated$.pipe(pluck('isAuthenticated'));
+ }
+
+ login() {
+ this.oidcSecurityService.authorize();
+ }
+
+ refreshSession() {
+ this.oidcSecurityService.forceRefreshSession().subscribe((result) => console.log(result));
+ }
+
+ logout() {
+ this.oidcSecurityService.logoff();
+ }
+
+ logoffAndRevokeTokens() {
+ this.oidcSecurityService.logoffAndRevokeTokens().subscribe((result) => console.log(result));
+ }
+
+ revokeRefreshToken() {
+ this.oidcSecurityService.revokeRefreshToken().subscribe((result) => console.log(result));
+ }
+
+ revokeAccessToken() {
+ this.oidcSecurityService.revokeAccessToken().subscribe((result) => console.log(result));
+ }
+
+}
diff --git a/src/app/auth/unauthorized/unauthorized.component.html b/src/app/auth/unauthorized/unauthorized.component.html
new file mode 100644
index 00000000..721676a4
--- /dev/null
+++ b/src/app/auth/unauthorized/unauthorized.component.html
@@ -0,0 +1,4 @@
+
+
Unauthorized
+
Code 403
+
diff --git a/src/app/auth/unauthorized/unauthorized.component.scss b/src/app/auth/unauthorized/unauthorized.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/auth/unauthorized/unauthorized.component.spec.ts b/src/app/auth/unauthorized/unauthorized.component.spec.ts
new file mode 100644
index 00000000..28f7beac
--- /dev/null
+++ b/src/app/auth/unauthorized/unauthorized.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UnauthorizedComponent } from './unauthorized.component';
+
+describe('UnauthorizedComponent', () => {
+ let component: UnauthorizedComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ UnauthorizedComponent ],
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UnauthorizedComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/auth/unauthorized/unauthorized.component.ts b/src/app/auth/unauthorized/unauthorized.component.ts
new file mode 100644
index 00000000..706d2d9f
--- /dev/null
+++ b/src/app/auth/unauthorized/unauthorized.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'wh-unauthorized',
+ templateUrl: './unauthorized.component.html',
+ styleUrls: ['./unauthorized.component.scss'],
+})
+export class UnauthorizedComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/src/app/auth/wharf-auth.interceptor.ts b/src/app/auth/wharf-auth.interceptor.ts
new file mode 100644
index 00000000..e41bf56d
--- /dev/null
+++ b/src/app/auth/wharf-auth.interceptor.ts
@@ -0,0 +1,38 @@
+import { Injectable } from '@angular/core';
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
+import { OidcSecurityService } from 'angular-auth-oidc-client';
+import { Observable } from 'rxjs';
+import { ConfigService } from '../shared/config/config.service';
+
+@Injectable()
+export class WharfAuthInterceptor implements HttpInterceptor {
+
+ constructor(
+ private oidcSecurityService: OidcSecurityService,
+ private configService: ConfigService,
+ ) {}
+
+ intercept(req: HttpRequest, next: HttpHandler): Observable> {
+ if (this.configService.hasConfig()){
+ const apiUrl = this.configService.getApiConfig().basePath;
+ if (req.url.includes(apiUrl)) {
+ const token = this.oidcSecurityService.getAccessToken();
+ if (!token) {
+ return next.handle(req);
+ }
+ const bearerToken = `Bearer ${token}`;
+ req = req.clone({
+ setHeaders: {
+ /* eslint-disable @typescript-eslint/naming-convention */
+ Authorization: bearerToken,
+ /* eslint-enable @typescript-eslint/naming-convention */
+ },
+ withCredentials: true,
+ });
+
+ return next.handle(req);
+ }
+ }
+ return next.handle(req);
+ }
+}
diff --git a/src/app/nav/nav.component.html b/src/app/nav/nav.component.html
index d1487de6..12d49a81 100644
--- a/src/app/nav/nav.component.html
+++ b/src/app/nav/nav.component.html
@@ -85,10 +85,9 @@ WHARF
+
-
-
+
diff --git a/src/app/nav/nav.component.ts b/src/app/nav/nav.component.ts
index 000e1bdf..00881db1 100644
--- a/src/app/nav/nav.component.ts
+++ b/src/app/nav/nav.component.ts
@@ -1,14 +1,17 @@
-import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MenuItem } from 'primeng/api';
import { environment } from 'src/environments/environment';
import { MetaService as GitLabMetaService } from 'import-gitlab-client';
import { MetaService as GitHubMetaService } from 'import-github-client';
import { MetaService as AzureDevOpsMetaService } from 'import-azuredevops-client';
import { MetaService as ApiMeta } from 'api-client';
-import { Observable } from 'rxjs';
+import { combineLatest, Observable, ReplaySubject } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { HttpErrorResponse } from '@angular/common/http';
import { LicensesService } from '../shared/licenses/licenses.service';
+import { Router } from '@angular/router';
+import { OidcSecurityService } from 'angular-auth-oidc-client';
+import { takeUntil } from 'rxjs/operators';
enum RemoteVersionStatus {
Pending,
@@ -39,10 +42,11 @@ enum ServiceName {
selector: 'wh-app-nav',
templateUrl: './nav.component.html',
})
-export class NavComponent implements OnInit {
+export class NavComponent implements OnInit, OnDestroy {
projectItem: MenuItem[];
items: MenuItem[];
documentationItem: MenuItem[];
+ loginItem: MenuItem[];
userItem: MenuItem[];
remoteVersionStatus = RemoteVersionStatus;
@@ -55,6 +59,7 @@ export class NavComponent implements OnInit {
];
private isFetchingVersions = false;
+ private isDestroyed$: ReplaySubject = new ReplaySubject(1);
constructor(
private gitLabMetaService: GitLabMetaService,
@@ -63,13 +68,16 @@ export class NavComponent implements OnInit {
private apiMeta: ApiMeta,
private ref: ChangeDetectorRef,
private licensesService: LicensesService,
- ) { }
+ private router: Router,
+ private oidcSecurityService: OidcSecurityService,
+ ) {
+ }
get env() {
return environment;
}
- ngOnInit() {
+ public ngOnInit() {
this.projectItem = [
{ label: 'PROJECTS', icon: 'pi pi-file-o', routerLink: ['/'] },
];
@@ -84,8 +92,38 @@ export class NavComponent implements OnInit {
];
this.userItem = [
+ { label: 'LOGIN', icon: 'pi pi-sign-in', command: () => this.oidcSecurityService.authorize() },
{ label: 'user.name', disabled: true, icon: 'pi pi-user' },
];
+
+ this.setMenuOptsAuth();
+ }
+
+ public ngOnDestroy() {
+ this.isDestroyed$.next(true);
+ this.isDestroyed$.complete();
+ }
+
+ private setMenuOptsAuth(): void {
+ combineLatest(this.oidcSecurityService.userData$, this.oidcSecurityService.isAuthenticated$)
+ .pipe(takeUntil(this.isDestroyed$))
+ .subscribe(authStatus => {
+ if (authStatus[1].isAuthenticated) {
+ this.userItem = [
+ { label: 'LOGOUT', icon: 'pi pi-sign-out', command: () => this.oidcSecurityService.logoff() },
+ {
+ label: authStatus[0].userData?.name,
+ icon: 'pi pi-user',
+ command: () => this.router.navigate(['/login']),
+ },
+ ];
+ } else {
+ this.userItem = [
+ { label: 'LOGIN', icon: 'pi pi-sign-in', command: () => this.oidcSecurityService.authorize() },
+ { label: 'user.name', disabled: true, icon: 'pi pi-user' },
+ ];
+ }
+ });
}
fetchServiceVersions() {
@@ -115,9 +153,10 @@ export class NavComponent implements OnInit {
return;
}
state.status = RemoteVersionStatus.Pending;
- version$.pipe(finalize(() => {
- this.ref.markForCheck();
- })).subscribe({
+ version$.pipe(
+ takeUntil(this.isDestroyed$),
+ finalize(() => this.ref.markForCheck(),
+ )).subscribe({
next: version => {
state.status = RemoteVersionStatus.OK;
state.version = version.version;
diff --git a/src/app/shared/config/config.service.ts b/src/app/shared/config/config.service.ts
index cb9fb06d..a8c3815f 100644
--- a/src/app/shared/config/config.service.ts
+++ b/src/app/shared/config/config.service.ts
@@ -3,6 +3,11 @@ import { Config } from './config';
import { Configuration } from 'api-client';
import { Configuration as GitlabConfiguration } from 'import-gitlab-client';
import { Configuration as AzureDevOpsConfiguration } from 'import-azuredevops-client';
+import { OpenIdConfiguration } from 'angular-auth-oidc-client';
+import { config, Observable, of, pluck } from 'rxjs';
+import { catchError, map, tap } from 'rxjs/operators';
+import { resolve } from '@angular/compiler-cli/src/ngtsc/file_system';
+import { HttpClient } from '@angular/common/http';
/**
* Backward compatability to allow config values to start with an uppercase or lowercase.
@@ -40,22 +45,34 @@ export const lowClone = (obj: T): T => {
return clone;
};
+const configUrl = 'assets/config.json';
+
@Injectable({
providedIn: 'root',
})
export class ConfigService {
private config: Config;
- constructor() { }
+ constructor(
+ private httpClient: HttpClient,
+ ) { }
setConfig(config: Config): void {
this.config = lowClone(config);
}
- getConfig(): Config {
+ public getConfig(): Config {
return this.config;
}
+ public getConfig$(): Observable {
+ return this.httpClient.get(configUrl);
+ }
+
+ hasConfig(): boolean {
+ return !! this.config;
+ }
+
getApiConfig(): Configuration {
return new Configuration({
basePath: upGet(upGet(this.config, 'backendUrls'), 'api'),
@@ -79,4 +96,11 @@ export class ConfigService {
basePath: upGet(upGet(this.config, 'backendUrls'), 'azureDevopsImport'),
});
}
+
+ public getOidcConfig$(): Observable {
+ return this.getConfig$().pipe(
+ map((configuration: Config) => configuration.oidcConfig),
+ );
+ }
+
}
diff --git a/src/app/shared/config/config.ts b/src/app/shared/config/config.ts
index dc53f926..13348c87 100644
--- a/src/app/shared/config/config.ts
+++ b/src/app/shared/config/config.ts
@@ -1,6 +1,9 @@
+import { OpenIdConfiguration } from 'angular-auth-oidc-client';
+
export interface Config {
environment: EnvironmentConfig;
backendUrls: BackendUrlsConfig;
+ oidcConfig: OpenIdConfiguration;
}
interface EnvironmentConfig {
diff --git a/src/assets/config.json b/src/assets/config.json
index 3e342db5..9c5fe40c 100644
--- a/src/assets/config.json
+++ b/src/assets/config.json
@@ -5,16 +5,25 @@
},
"updateLatency": 20000,
"updateFrequency": 30000,
- "identityServer": {
- "url": "https://login.stage.local",
- "scope": "openid profile email SpotApi",
- "debugMode": false,
- "silentRenewEndpoint": "/assets/silent-renew.html"
- },
"backendUrls": {
- "Api": "http://localhost:5001/api",
- "GitlabImport": "http://localhost:5002/import",
- "GithubImport": "http://localhost:5003/import",
- "AzureDevopsImport": "http://localhost:5004/import"
+ "Api": "http://localhost:4200/api",
+ "GitlabImport": "http://localhost:4200/import",
+ "GithubImport": "http://localhost:4200/import",
+ "AzureDevopsImport": "http://localhost:4200/import"
+ },
+ "oidcConfig": {
+ "authority": "https://login.microsoftonline.com/841df554-ef9d-48b1-bc6e-44cf8543a8fc/v2.0/.well-known/openid-configuration",
+ "redirectUrl": "http://localhost:4200",
+ "postLogoutRedirectUri": "http://localhost:4200",
+ "clientId": "01fcb3dc-7a2b-4b1c-a7d6-d7033089c779",
+ "scope": "openid profile email offline_access api://wharf-internal/read api://wharf-internal/admin api://wharf-internal/deploy",
+ "responseType": "id_token token",
+ "ignoreNonceAfterRefresh": true,
+ "silentRenew": true,
+ "useRefreshToken": true,
+ "logLevel": "debug",
+ "maxIdTokenIatOffsetAllowedInSeconds": 600,
+ "issValidationOff": false,
+ "autoUserInfo": false
}
}
diff --git a/src/proxy/docker.dev.conf.json b/src/proxy/docker.dev.conf.json
new file mode 100644
index 00000000..1da556ab
--- /dev/null
+++ b/src/proxy/docker.dev.conf.json
@@ -0,0 +1,12 @@
+{
+ "/api/*": {
+ "target": "http://localhost:5000",
+ "secure": false,
+ "logLevel": "debug"
+ },
+ "/import/*": {
+ "target": "http://localhost:5000",
+ "secure": false,
+ "logLevel": "debug"
+ }
+}
diff --git a/src/proxy/local.dev.conf.json b/src/proxy/local.dev.conf.json
new file mode 100644
index 00000000..36abe2fe
--- /dev/null
+++ b/src/proxy/local.dev.conf.json
@@ -0,0 +1,22 @@
+{
+ "/api/*": {
+ "target": "http://localhost:5001",
+ "secure": false,
+ "logLevel": "debug"
+ },
+ "/import/gitlab/*": {
+ "target": "http://localhost:5002",
+ "secure": false,
+ "logLevel": "debug"
+ },
+ "/import/github/*": {
+ "target": "http://localhost:5003",
+ "secure": false,
+ "logLevel": "debug"
+ },
+ "/import/azuredevops/*": {
+ "target": "http://localhost:5004",
+ "secure": false,
+ "logLevel": "debug"
+ }
+}