working frontend, adding classes for type matching.
Added Dashboard component (will act as true dashboard from now on).
This commit is contained in:
parent
e657d73e0e
commit
745768e2e6
|
@ -1,15 +1,13 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
// import 'fhir-js-client';
|
|
||||||
import * as Oauth from '@panva/oauth4webapi';
|
import * as Oauth from '@panva/oauth4webapi';
|
||||||
import { concatMap, delay, retryWhen } from 'rxjs/operators';
|
import { concatMap, delay, retryWhen } from 'rxjs/operators';
|
||||||
import { Observable, of, throwError } from 'rxjs';
|
import { Observable, of, throwError } from 'rxjs';
|
||||||
// import {fhirclient} from 'fhirclient/lib/types';
|
|
||||||
import * as FHIR from "fhirclient"
|
import * as FHIR from "fhirclient"
|
||||||
import Client from 'fhirclient/lib/Client';
|
|
||||||
import {getAccessTokenExpiration} from 'fhirclient/lib/lib';
|
import {getAccessTokenExpiration} from 'fhirclient/lib/lib';
|
||||||
import BrowserAdapter from 'fhirclient/lib/adapters/BrowserAdapter';
|
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 retryCount = 24; //wait 2 minutes (5 * 24 = 120)
|
||||||
export const retryWaitMilliSeconds = 5000; //wait 5 seconds
|
export const retryWaitMilliSeconds = 5000; //wait 5 seconds
|
||||||
|
|
||||||
|
@ -22,37 +20,27 @@ export class AppComponent {
|
||||||
title = 'fastenhealth';
|
title = 'fastenhealth';
|
||||||
|
|
||||||
|
|
||||||
constructor(private http: HttpClient) { }
|
constructor(private passportApi: PassportService) { }
|
||||||
|
|
||||||
connect(provider: string) {
|
connect(provider: string) {
|
||||||
this.http.get<any>(`https://sandbox-api.fastenhealth.com/v1/connect/${provider}`)
|
this.passportApi.getProviderConfig(provider)
|
||||||
.subscribe(async (connectData: any) => {
|
.subscribe(async (connectData: ProviderConfig) => {
|
||||||
console.log(connectData);
|
console.log(connectData);
|
||||||
|
|
||||||
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
|
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
|
||||||
const codeVerifier = Oauth.generateRandomCodeVerifier();
|
const codeVerifier = Oauth.generateRandomCodeVerifier();
|
||||||
const codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier);
|
const codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier);
|
||||||
const codeChallengeMethod = 'S256';
|
const codeChallengeMethod = 'S256';
|
||||||
|
|
||||||
// generate the authorization url
|
|
||||||
const state = this.uuidV4()
|
const state = this.uuidV4()
|
||||||
const authorizationUrl = new URL(`${connectData.message.oauth_endpoint_base_url}/authorize`);
|
|
||||||
authorizationUrl.searchParams.set('client_id', connectData.message.client_id);
|
const authorizationUrl = this.passportApi.generatePKCEProviderAuthorizeUrl(codeVerifier, codeChallenge, codeChallengeMethod, state, connectData)
|
||||||
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);
|
|
||||||
}
|
|
||||||
console.log('authorize url:', authorizationUrl.toString());
|
console.log('authorize url:', authorizationUrl.toString());
|
||||||
// open new browser window
|
// open new browser window
|
||||||
window.open(authorizationUrl.toString(), "_blank");
|
window.open(authorizationUrl.toString(), "_blank");
|
||||||
|
|
||||||
//wait for response
|
//wait for response
|
||||||
this.waitForClaimOrTimeout(provider, state).subscribe(async (claimData: any) => {
|
this.waitForClaimOrTimeout(provider, state).subscribe(async (claimData: AuthorizeClaim) => {
|
||||||
console.log("claim response:", claimData)
|
console.log("claim response:", claimData)
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,19 +50,19 @@ export class AppComponent {
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const client: oauth.Client = {
|
const client: oauth.Client = {
|
||||||
client_id: connectData.message.client_id,
|
client_id: connectData.client_id,
|
||||||
token_endpoint_auth_method: 'none',
|
token_endpoint_auth_method: 'none',
|
||||||
}
|
}
|
||||||
|
|
||||||
const as = {
|
const as = {
|
||||||
issuer: `${authorizationUrl.protocol}//${authorizationUrl.host}`,
|
issuer: `${authorizationUrl.protocol}//${authorizationUrl.host}`,
|
||||||
authorization_endpoint: `${connectData.message.oauth_endpoint_base_url}/authorize`,
|
authorization_endpoint: `${connectData.oauth_endpoint_base_url}/authorize`,
|
||||||
token_endpoint: `${connectData.message.oauth_endpoint_base_url}/token`,
|
token_endpoint: `${connectData.oauth_endpoint_base_url}/token`,
|
||||||
introspect_endpoint: `${connectData.message.oauth_endpoint_base_url}/introspect`,
|
introspect_endpoint: `${connectData.oauth_endpoint_base_url}/introspect`,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("STARTING--- Oauth.validateAuthResponse")
|
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)) {
|
if (Oauth.isOAuth2Error(params)) {
|
||||||
console.log('error', params)
|
console.log('error', params)
|
||||||
throw new Error() // Handle OAuth 2.0 redirect error
|
throw new Error() // Handle OAuth 2.0 redirect error
|
||||||
|
@ -85,7 +73,7 @@ export class AppComponent {
|
||||||
as,
|
as,
|
||||||
client,
|
client,
|
||||||
params,
|
params,
|
||||||
connectData.message.redirect_uri,
|
connectData.redirect_uri,
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
)
|
)
|
||||||
const payload = await response.json()
|
const payload = await response.json()
|
||||||
|
@ -94,11 +82,11 @@ export class AppComponent {
|
||||||
|
|
||||||
//Create FHIR Client
|
//Create FHIR Client
|
||||||
const clientState = {
|
const clientState = {
|
||||||
serverUrl: connectData.message.api_endpoint_base_url,
|
serverUrl: connectData.api_endpoint_base_url,
|
||||||
clientId: connectData.message.client_id,
|
clientId: connectData.client_id,
|
||||||
redirectUri: connectData.message.redirect_uri,
|
redirectUri: connectData.redirect_uri,
|
||||||
tokenUri: `${connectData.message.oauth_endpoint_base_url}/token`,
|
tokenUri: `${connectData.oauth_endpoint_base_url}/token`,
|
||||||
scope: connectData.message.scopes.join(' '),
|
scope: connectData.scopes.join(' '),
|
||||||
tokenResponse: payload,
|
tokenResponse: payload,
|
||||||
expiresAt: getAccessTokenExpiration(payload, new BrowserAdapter()),
|
expiresAt: getAccessTokenExpiration(payload, new BrowserAdapter()),
|
||||||
codeChallenge: codeChallenge,
|
codeChallenge: codeChallenge,
|
||||||
|
@ -136,8 +124,8 @@ export class AppComponent {
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
waitForClaimOrTimeout(provider: string, state: string): Observable<any> {
|
waitForClaimOrTimeout(providerId: string, state: string): Observable<any> {
|
||||||
return this.http.get<any>(`https://sandbox-api.fastenhealth.com/v1/claim/${provider}`, {params: {"state": state}}).pipe(
|
return this.passportApi.getProviderAuthorizeClaim(providerId, state).pipe(
|
||||||
retryWhen(error =>
|
retryWhen(error =>
|
||||||
error.pipe(
|
error.pipe(
|
||||||
concatMap((error, count) => {
|
concatMap((error, count) => {
|
||||||
|
|
|
@ -4,10 +4,12 @@ import { NgModule } from '@angular/core';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent
|
AppComponent,
|
||||||
|
DashboardComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<p>dashboard works!</p>
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DashboardComponent } from './dashboard.component';
|
||||||
|
|
||||||
|
describe('DashboardComponent', () => {
|
||||||
|
let component: DashboardComponent;
|
||||||
|
let fixture: ComponentFixture<DashboardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ DashboardComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DashboardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { AuthorizeClaim } from './authorize-claim';
|
||||||
|
|
||||||
|
describe('AuthorizeClaim', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
expect(new AuthorizeClaim()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
export class AuthorizeClaim {
|
||||||
|
providerId: string
|
||||||
|
state: string
|
||||||
|
code: string
|
||||||
|
ttl: number
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ProviderConfig } from './provider-config';
|
||||||
|
|
||||||
|
describe('ProviderConfig', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
expect(new ProviderConfig()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ResponseWrapper } from './response-wrapper';
|
||||||
|
|
||||||
|
describe('ResponseWrapper', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
expect(new ResponseWrapper()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
export class ResponseWrapper {
|
||||||
|
data: any
|
||||||
|
success: boolean
|
||||||
|
error: string
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class FastenApiService {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<ProviderConfig> {
|
||||||
|
return this._httpClient.get<any>(`${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<AuthorizeClaim> {
|
||||||
|
return this._httpClient.get<any>(`${environment.fasten_api_endpoint_base}/v1/claim/${providerId}`, {params: {"state": state}})
|
||||||
|
.pipe(
|
||||||
|
map((response: ResponseWrapper) => {
|
||||||
|
return response.data as AuthorizeClaim
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,7 +3,8 @@
|
||||||
// The list of file replacements can be found in `angular.json`.
|
// The list of file replacements can be found in `angular.json`.
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false
|
production: false,
|
||||||
|
fasten_api_endpoint_base: 'https://sandbox-api.fastenhealth.com'
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Reference in New Issue