From 745768e2e6cc65e4b73facc8f38d6516c825129c Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 25 Aug 2022 21:07:33 -0700 Subject: [PATCH] working frontend, adding classes for type matching. Added Dashboard component (will act as true dashboard from now on). --- frontend/src/app/app.component.ts | 58 ++++++++----------- frontend/src/app/app.module.ts | 4 +- .../app/dashboard/dashboard.component.html | 1 + .../app/dashboard/dashboard.component.scss | 0 .../app/dashboard/dashboard.component.spec.ts | 23 ++++++++ .../src/app/dashboard/dashboard.component.ts | 15 +++++ .../models/passport/authorize-claim.spec.ts | 7 +++ .../app/models/passport/authorize-claim.ts | 6 ++ .../models/passport/provider-config.spec.ts | 7 +++ .../app/models/passport/provider-config.ts | 9 +++ .../src/app/models/response-wrapper.spec.ts | 7 +++ frontend/src/app/models/response-wrapper.ts | 5 ++ .../app/services/fasten-api.service.spec.ts | 16 +++++ .../src/app/services/fasten-api.service.ts | 9 +++ .../src/app/services/passport.service.spec.ts | 16 +++++ frontend/src/app/services/passport.service.ts | 52 +++++++++++++++++ frontend/src/environments/environment.ts | 3 +- 17 files changed, 201 insertions(+), 37 deletions(-) create mode 100644 frontend/src/app/dashboard/dashboard.component.html create mode 100644 frontend/src/app/dashboard/dashboard.component.scss create mode 100644 frontend/src/app/dashboard/dashboard.component.spec.ts create mode 100644 frontend/src/app/dashboard/dashboard.component.ts create mode 100644 frontend/src/app/models/passport/authorize-claim.spec.ts create mode 100644 frontend/src/app/models/passport/authorize-claim.ts create mode 100644 frontend/src/app/models/passport/provider-config.spec.ts create mode 100644 frontend/src/app/models/passport/provider-config.ts create mode 100644 frontend/src/app/models/response-wrapper.spec.ts create mode 100644 frontend/src/app/models/response-wrapper.ts create mode 100644 frontend/src/app/services/fasten-api.service.spec.ts create mode 100644 frontend/src/app/services/fasten-api.service.ts create mode 100644 frontend/src/app/services/passport.service.spec.ts create mode 100644 frontend/src/app/services/passport.service.ts diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index c9e57e82..717f6976 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,15 +1,13 @@ import { Component } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -// import 'fhir-js-client'; import * as Oauth from '@panva/oauth4webapi'; import { concatMap, delay, retryWhen } from 'rxjs/operators'; import { Observable, of, throwError } from 'rxjs'; -// import {fhirclient} from 'fhirclient/lib/types'; import * as FHIR from "fhirclient" -import Client from 'fhirclient/lib/Client'; import {getAccessTokenExpiration} from 'fhirclient/lib/lib'; import BrowserAdapter from 'fhirclient/lib/adapters/BrowserAdapter'; - +import {PassportService} from './services/passport.service'; +import {ProviderConfig} from './models/passport/provider-config'; +import {AuthorizeClaim} from './models/passport/authorize-claim'; export const retryCount = 24; //wait 2 minutes (5 * 24 = 120) export const retryWaitMilliSeconds = 5000; //wait 5 seconds @@ -22,37 +20,27 @@ export class AppComponent { title = 'fastenhealth'; - constructor(private http: HttpClient) { } + constructor(private passportApi: PassportService) { } connect(provider: string) { - this.http.get(`https://sandbox-api.fastenhealth.com/v1/connect/${provider}`) - .subscribe(async (connectData: any) => { + this.passportApi.getProviderConfig(provider) + .subscribe(async (connectData: ProviderConfig) => { console.log(connectData); // https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts const codeVerifier = Oauth.generateRandomCodeVerifier(); const codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier); const codeChallengeMethod = 'S256'; - - // generate the authorization url const state = this.uuidV4() - const authorizationUrl = new URL(`${connectData.message.oauth_endpoint_base_url}/authorize`); - authorizationUrl.searchParams.set('client_id', connectData.message.client_id); - authorizationUrl.searchParams.set('code_challenge', codeChallenge); - authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod); - authorizationUrl.searchParams.set('redirect_uri', connectData.message.redirect_uri); - authorizationUrl.searchParams.set('response_type', 'code'); - authorizationUrl.searchParams.set('scope', connectData.message.scopes.join(' ')); - authorizationUrl.searchParams.set('state', state); - if (connectData.message.aud){ - authorizationUrl.searchParams.set('aud', connectData.message.aud); - } + + const authorizationUrl = this.passportApi.generatePKCEProviderAuthorizeUrl(codeVerifier, codeChallenge, codeChallengeMethod, state, connectData) + console.log('authorize url:', authorizationUrl.toString()); // open new browser window window.open(authorizationUrl.toString(), "_blank"); //wait for response - this.waitForClaimOrTimeout(provider, state).subscribe(async (claimData: any) => { + this.waitForClaimOrTimeout(provider, state).subscribe(async (claimData: AuthorizeClaim) => { console.log("claim response:", claimData) @@ -62,19 +50,19 @@ export class AppComponent { // @ts-expect-error const client: oauth.Client = { - client_id: connectData.message.client_id, + client_id: connectData.client_id, token_endpoint_auth_method: 'none', } const as = { issuer: `${authorizationUrl.protocol}//${authorizationUrl.host}`, - authorization_endpoint: `${connectData.message.oauth_endpoint_base_url}/authorize`, - token_endpoint: `${connectData.message.oauth_endpoint_base_url}/token`, - introspect_endpoint: `${connectData.message.oauth_endpoint_base_url}/introspect`, + authorization_endpoint: `${connectData.oauth_endpoint_base_url}/authorize`, + token_endpoint: `${connectData.oauth_endpoint_base_url}/token`, + introspect_endpoint: `${connectData.oauth_endpoint_base_url}/introspect`, } console.log("STARTING--- Oauth.validateAuthResponse") - const params = Oauth.validateAuthResponse(as, client, new URLSearchParams(claimData.message), state) + const params = Oauth.validateAuthResponse(as, client, new URLSearchParams(claimData as any), state) if (Oauth.isOAuth2Error(params)) { console.log('error', params) throw new Error() // Handle OAuth 2.0 redirect error @@ -85,7 +73,7 @@ export class AppComponent { as, client, params, - connectData.message.redirect_uri, + connectData.redirect_uri, codeVerifier, ) const payload = await response.json() @@ -94,11 +82,11 @@ export class AppComponent { //Create FHIR Client const clientState = { - serverUrl: connectData.message.api_endpoint_base_url, - clientId: connectData.message.client_id, - redirectUri: connectData.message.redirect_uri, - tokenUri: `${connectData.message.oauth_endpoint_base_url}/token`, - scope: connectData.message.scopes.join(' '), + serverUrl: connectData.api_endpoint_base_url, + clientId: connectData.client_id, + redirectUri: connectData.redirect_uri, + tokenUri: `${connectData.oauth_endpoint_base_url}/token`, + scope: connectData.scopes.join(' '), tokenResponse: payload, expiresAt: getAccessTokenExpiration(payload, new BrowserAdapter()), codeChallenge: codeChallenge, @@ -136,8 +124,8 @@ export class AppComponent { }); } - waitForClaimOrTimeout(provider: string, state: string): Observable { - return this.http.get(`https://sandbox-api.fastenhealth.com/v1/claim/${provider}`, {params: {"state": state}}).pipe( + waitForClaimOrTimeout(providerId: string, state: string): Observable { + return this.passportApi.getProviderAuthorizeClaim(providerId, state).pipe( retryWhen(error => error.pipe( concatMap((error, count) => { diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index c6494bca..4cf7d6af 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -4,10 +4,12 @@ import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { HttpClientModule } from '@angular/common/http'; +import { DashboardComponent } from './dashboard/dashboard.component'; @NgModule({ declarations: [ - AppComponent + AppComponent, + DashboardComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html new file mode 100644 index 00000000..9c5fce97 --- /dev/null +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -0,0 +1 @@ +

dashboard works!

diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/dashboard/dashboard.component.spec.ts b/frontend/src/app/dashboard/dashboard.component.spec.ts new file mode 100644 index 00000000..6e4dcd89 --- /dev/null +++ b/frontend/src/app/dashboard/dashboard.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DashboardComponent } from './dashboard.component'; + +describe('DashboardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DashboardComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts new file mode 100644 index 00000000..843c80f8 --- /dev/null +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'] +}) +export class DashboardComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/models/passport/authorize-claim.spec.ts b/frontend/src/app/models/passport/authorize-claim.spec.ts new file mode 100644 index 00000000..619e5cf2 --- /dev/null +++ b/frontend/src/app/models/passport/authorize-claim.spec.ts @@ -0,0 +1,7 @@ +import { AuthorizeClaim } from './authorize-claim'; + +describe('AuthorizeClaim', () => { + it('should create an instance', () => { + expect(new AuthorizeClaim()).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/models/passport/authorize-claim.ts b/frontend/src/app/models/passport/authorize-claim.ts new file mode 100644 index 00000000..1e331a60 --- /dev/null +++ b/frontend/src/app/models/passport/authorize-claim.ts @@ -0,0 +1,6 @@ +export class AuthorizeClaim { + providerId: string + state: string + code: string + ttl: number +} diff --git a/frontend/src/app/models/passport/provider-config.spec.ts b/frontend/src/app/models/passport/provider-config.spec.ts new file mode 100644 index 00000000..74ef9b4b --- /dev/null +++ b/frontend/src/app/models/passport/provider-config.spec.ts @@ -0,0 +1,7 @@ +import { ProviderConfig } from './provider-config'; + +describe('ProviderConfig', () => { + it('should create an instance', () => { + expect(new ProviderConfig()).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/models/passport/provider-config.ts b/frontend/src/app/models/passport/provider-config.ts new file mode 100644 index 00000000..94d37ae7 --- /dev/null +++ b/frontend/src/app/models/passport/provider-config.ts @@ -0,0 +1,9 @@ +export class ProviderConfig { + oauth_endpoint_base_url: string + api_endpoint_base_url: string + response_type: string + client_id: string + scopes: string[] + redirect_uri: string + aud: string +} diff --git a/frontend/src/app/models/response-wrapper.spec.ts b/frontend/src/app/models/response-wrapper.spec.ts new file mode 100644 index 00000000..d35669e7 --- /dev/null +++ b/frontend/src/app/models/response-wrapper.spec.ts @@ -0,0 +1,7 @@ +import { ResponseWrapper } from './response-wrapper'; + +describe('ResponseWrapper', () => { + it('should create an instance', () => { + expect(new ResponseWrapper()).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/models/response-wrapper.ts b/frontend/src/app/models/response-wrapper.ts new file mode 100644 index 00000000..9b528500 --- /dev/null +++ b/frontend/src/app/models/response-wrapper.ts @@ -0,0 +1,5 @@ +export class ResponseWrapper { + data: any + success: boolean + error: string +} diff --git a/frontend/src/app/services/fasten-api.service.spec.ts b/frontend/src/app/services/fasten-api.service.spec.ts new file mode 100644 index 00000000..1883b5df --- /dev/null +++ b/frontend/src/app/services/fasten-api.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { FastenApiService } from './fasten-api.service'; + +describe('FastenApiService', () => { + let service: FastenApiService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FastenApiService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts new file mode 100644 index 00000000..b9538b43 --- /dev/null +++ b/frontend/src/app/services/fasten-api.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class FastenApiService { + + constructor() { } +} diff --git a/frontend/src/app/services/passport.service.spec.ts b/frontend/src/app/services/passport.service.spec.ts new file mode 100644 index 00000000..6893ae89 --- /dev/null +++ b/frontend/src/app/services/passport.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { PassportService } from './passport.service'; + +describe('PassportService', () => { + let service: PassportService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PassportService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/passport.service.ts b/frontend/src/app/services/passport.service.ts new file mode 100644 index 00000000..8b873d95 --- /dev/null +++ b/frontend/src/app/services/passport.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Observable} from 'rxjs'; +import {environment} from '../../environments/environment'; +import {map, tap} from 'rxjs/operators'; +import {ResponseWrapper} from '../models/response-wrapper'; +import {ProviderConfig} from '../models/passport/provider-config'; +import {AuthorizeClaim} from '../models/passport/authorize-claim'; + +@Injectable({ + providedIn: 'root' +}) +export class PassportService { + + constructor(private _httpClient: HttpClient) { + } + + getProviderConfig(providerId: string): Observable { + return this._httpClient.get(`${environment.fasten_api_endpoint_base}/v1/connect/${providerId}`) + .pipe( + map((response: ResponseWrapper) => { + return response.data as ProviderConfig + }) + ); + } + + generatePKCEProviderAuthorizeUrl(codeVerifier: string, codeChallenge: string, codeChallengeMethod: string, state: string, providerConfig: ProviderConfig): URL { + // generate the authorization url + const authorizationUrl = new URL(`${providerConfig.oauth_endpoint_base_url}/authorize`); + authorizationUrl.searchParams.set('client_id', providerConfig.client_id); + authorizationUrl.searchParams.set('code_challenge', codeChallenge); + authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod); + authorizationUrl.searchParams.set('redirect_uri', providerConfig.redirect_uri); + authorizationUrl.searchParams.set('response_type', 'code'); + authorizationUrl.searchParams.set('scope', providerConfig.scopes.join(' ')); + authorizationUrl.searchParams.set('state', state); + if (providerConfig.aud) { + authorizationUrl.searchParams.set('aud', providerConfig.aud); + } + return authorizationUrl + } + + getProviderAuthorizeClaim(providerId: string, state: string): Observable { + return this._httpClient.get(`${environment.fasten_api_endpoint_base}/v1/claim/${providerId}`, {params: {"state": state}}) + .pipe( + map((response: ResponseWrapper) => { + return response.data as AuthorizeClaim + }) + ); + } + +} diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 7b4f817a..07bcfc8b 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -3,7 +3,8 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + fasten_api_endpoint_base: 'https://sandbox-api.fastenhealth.com' }; /*