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 @@ +
+

Forbidden

+

Code 403

+
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" + } +}