diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 71c7dbbd..0680b43f 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 4abad5fc..957feeb1 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,17 +1,4 @@ import { Component } from '@angular/core'; -import * as Oauth from '@panva/oauth4webapi'; -import { concatMap, delay, retryWhen } from 'rxjs/operators'; -import { Observable, of, throwError } from 'rxjs'; -import * as FHIR from "fhirclient" -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'; -import {FastenApiService} from './services/fasten-api.service'; -import {ProviderCredential} from './models/fasten/provider-credential'; -export const retryCount = 24; //wait 2 minutes (5 * 24 = 120) -export const retryWaitMilliSeconds = 5000; //wait 5 seconds @Component({ selector: 'app-root', @@ -20,148 +7,6 @@ export const retryWaitMilliSeconds = 5000; //wait 5 seconds }) export class AppComponent { title = 'fastenhealth'; - - - constructor( - private passportApi: PassportService, - private fastenApi: FastenApiService, - ) { } - - connect(provider: string) { - 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'; - const state = this.uuidV4() - - 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: AuthorizeClaim) => { - console.log("claim response:", claimData) - - - //swap code for token - let sub: string - let access_token: string - - // @ts-expect-error - const client: oauth.Client = { - client_id: connectData.client_id, - token_endpoint_auth_method: 'none', - } - - const as = { - issuer: `${authorizationUrl.protocol}//${authorizationUrl.host}`, - 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 as any), state) - if (Oauth.isOAuth2Error(params)) { - console.log('error', params) - throw new Error() // Handle OAuth 2.0 redirect error - } - console.log("ENDING--- Oauth.validateAuthResponse") - console.log("STARTING--- Oauth.authorizationCodeGrantRequest") - const response = await Oauth.authorizationCodeGrantRequest( - as, - client, - params, - connectData.redirect_uri, - codeVerifier, - ) - const payload = await response.json() - console.log("ENDING--- Oauth.authorizationCodeGrantRequest", payload) - - - //Create FHIR Client - const providerCredential: ProviderCredential = { - oauth_endpoint_base_url: connectData.oauth_endpoint_base_url, - api_endpoint_base_url: connectData.api_endpoint_base_url, - client_id: connectData.client_id, - redirect_uri: connectData.redirect_uri, - scopes: connectData.scopes.join(' '), - patient: payload.patient, - access_token: payload.access_token, - refresh_token: payload.refresh_token, - id_token: payload.id_token, - expires_at: getAccessTokenExpiration(payload, new BrowserAdapter()), - code_challenge: codeChallenge, - code_verifier: codeVerifier, - } - - this.fastenApi.createProviderCredential(providerCredential).subscribe( (respData) => { - console.log("provider credential create response:", respData) - }) - - - - - // console.log("STARTING--- FHIR.client(clientState)", clientState) - // const fhirClient = FHIR.client(clientState); - // - // console.log("STARTING--- client.request(Patient)") - // const patientResponse = await fhirClient.request("PatientAccess/v1/$userinfo") - // console.log(patientResponse) - - - - - - // // fetch userinfo response - // - // const response = await oauth.userInfoRequest(as, client, access_token) - // - // let challenges: oauth.WWWAuthenticateChallenge[] | undefined - // if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { - // for (const challenge of challenges) { - // console.log('challenge', challenge) - // } - // throw new Error() // Handle www-authenticate challenges as needed - // } - // - // const result = await oauth.processUserInfoResponse(as, client, sub, response) - // console.log('result', result) - - - - }) - - }); - } - waitForClaimOrTimeout(providerId: string, state: string): Observable { - return this.passportApi.getProviderAuthorizeClaim(providerId, state).pipe( - retryWhen(error => - error.pipe( - concatMap((error, count) => { - if (count <= retryCount && error.status == 500) { - return of(error); - } - return throwError(error); - }), - delay(retryWaitMilliSeconds) - ) - ) - ) - } - - uuidV4(){ - // @ts-ignore - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => - (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) - ); - } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index cb973f05..4c2b55cf 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -4,7 +4,6 @@ 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 './pages/dashboard/dashboard.component'; import { AdminLayoutComponent } from "./layouts/admin-layout/admin-layout.component"; import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; import { ComponentsModule } from "./components/components.module"; diff --git a/frontend/src/app/components/sidebar/sidebar.component.ts b/frontend/src/app/components/sidebar/sidebar.component.ts index 061c24b5..bd67a3c9 100644 --- a/frontend/src/app/components/sidebar/sidebar.component.ts +++ b/frontend/src/app/components/sidebar/sidebar.component.ts @@ -3,7 +3,6 @@ import { Component, OnInit } from "@angular/core"; declare interface RouteInfo { path: string; title: string; - rtlTitle: string; icon: string; class: string; } @@ -11,58 +10,32 @@ export const ROUTES: RouteInfo[] = [ { path: "/dashboard", title: "Dashboard", - rtlTitle: "لوحة القيادة", icon: "icon-chart-pie-36", class: "" }, { - path: "/icons", - title: "Icons", - rtlTitle: "الرموز", - icon: "icon-atom", + path: "/providers", + title: "Medical Providers", + icon: "icon-cloud-download-93", class: "" }, { - path: "/maps", - title: "Maps", - rtlTitle: "خرائط", - icon: "icon-pin", - class: "" }, - { - path: "/notifications", - title: "Notifications", - rtlTitle: "إخطارات", - icon: "icon-bell-55", - class: "" - }, - - { - path: "/user", - title: "User Profile", - rtlTitle: "ملف تعريفي للمستخدم", + path: "/patient", + title: "Patient Profile", icon: "icon-single-02", class: "" }, { path: "/tables", title: "Table List", - rtlTitle: "قائمة الجدول", icon: "icon-puzzle-10", class: "" }, { path: "/typography", title: "Typography", - rtlTitle: "طباعة", icon: "icon-align-center", class: "" - }, - { - path: "/rtl", - title: "RTL Support", - rtlTitle: "ار تي ال", - icon: "icon-world", - class: "" } ]; diff --git a/frontend/src/app/layouts/admin-layout/admin-layout.module.ts b/frontend/src/app/layouts/admin-layout/admin-layout.module.ts index ca3f395d..f4b36ab2 100644 --- a/frontend/src/app/layouts/admin-layout/admin-layout.module.ts +++ b/frontend/src/app/layouts/admin-layout/admin-layout.module.ts @@ -6,8 +6,9 @@ import { FormsModule } from "@angular/forms"; import { AdminLayoutRoutes } from "./admin-layout.routing"; import { DashboardComponent } from "../../pages/dashboard/dashboard.component"; -import { UserComponent } from "../../pages/user/user.component"; -// import { RtlComponent } from "../../pages/rtl/rtl.component"; +import { PatientComponent } from "../../pages/patient/patient.component"; +import { MedicalProvidersComponent } from "../../pages/medical-providers/medical-providers.component"; + import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; @@ -21,7 +22,8 @@ import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; ], declarations: [ DashboardComponent, - UserComponent, + PatientComponent, + MedicalProvidersComponent ] }) export class AdminLayoutModule {} diff --git a/frontend/src/app/layouts/admin-layout/admin-layout.routing.ts b/frontend/src/app/layouts/admin-layout/admin-layout.routing.ts index d12b64ed..3de94084 100644 --- a/frontend/src/app/layouts/admin-layout/admin-layout.routing.ts +++ b/frontend/src/app/layouts/admin-layout/admin-layout.routing.ts @@ -1,10 +1,11 @@ import { Routes } from "@angular/router"; import { DashboardComponent } from "../../pages/dashboard/dashboard.component"; -import { UserComponent } from "../../pages/user/user.component"; -// import { RtlComponent } from "../../pages/rtl/rtl.component"; +import { PatientComponent } from "../../pages/patient/patient.component"; +import {MedicalProvidersComponent} from '../../pages/medical-providers/medical-providers.component'; export const AdminLayoutRoutes: Routes = [ { path: "dashboard", component: DashboardComponent }, - { path: "user", component: UserComponent }, + { path: "patient", component: PatientComponent }, + { path: "providers", component: MedicalProvidersComponent }, ]; diff --git a/frontend/src/app/pages/medical-providers/medical-providers.component.html b/frontend/src/app/pages/medical-providers/medical-providers.component.html new file mode 100644 index 00000000..5f3a4589 --- /dev/null +++ b/frontend/src/app/pages/medical-providers/medical-providers.component.html @@ -0,0 +1,62 @@ +
+
+
+
+
+
Medical Providers
+

+ The following medical providers have API's which Fasten can use to retrieve your medical history. + Please click the logos below to initiate the connection. +

+
+
+
+ +
+
+ +

Aetna

+
+
+ + +
+
+ +

Anthem

+
+
+ +
+
+ +

Cigna

+
+
+ +
+
+ +

Humana

+
+
+ +
+
+ +

Kaiser

+
+
+ +
+
+ +

United Healthcare

+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/pages/medical-providers/medical-providers.component.scss b/frontend/src/app/pages/medical-providers/medical-providers.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/pages/medical-providers/medical-providers.component.spec.ts b/frontend/src/app/pages/medical-providers/medical-providers.component.spec.ts new file mode 100644 index 00000000..f9ca6bed --- /dev/null +++ b/frontend/src/app/pages/medical-providers/medical-providers.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MedicalProvidersComponent } from './medical-providers.component'; + +describe('MedicalProvidersComponent', () => { + let component: MedicalProvidersComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MedicalProvidersComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MedicalProvidersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/medical-providers/medical-providers.component.ts b/frontend/src/app/pages/medical-providers/medical-providers.component.ts new file mode 100644 index 00000000..44a7f2be --- /dev/null +++ b/frontend/src/app/pages/medical-providers/medical-providers.component.ts @@ -0,0 +1,167 @@ +import { Component, OnInit } from '@angular/core'; +import {PassportService} from '../../services/passport.service'; +import {FastenApiService} from '../../services/fasten-api.service'; +import {ProviderConfig} from '../../models/passport/provider-config'; +import * as Oauth from '@panva/oauth4webapi'; +import {AuthorizeClaim} from '../../models/passport/authorize-claim'; +import {ProviderCredential} from '../../models/fasten/provider-credential'; +import {getAccessTokenExpiration} from 'fhirclient/lib/lib'; +import BrowserAdapter from 'fhirclient/lib/adapters/BrowserAdapter'; +import {Observable, of, throwError} from 'rxjs'; +import {concatMap, delay, retryWhen} from 'rxjs/operators'; +import * as FHIR from "fhirclient" + +export const retryCount = 24; //wait 2 minutes (5 * 24 = 120) +export const retryWaitMilliSeconds = 5000; //wait 5 seconds + +@Component({ + selector: 'app-medical-providers', + templateUrl: './medical-providers.component.html', + styleUrls: ['./medical-providers.component.scss'] +}) +export class MedicalProvidersComponent implements OnInit { + + constructor( + private passportApi: PassportService, + private fastenApi: FastenApiService, + ) { } + + ngOnInit(): void { + } + + connect(provider: string) { + 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'; + const state = this.uuidV4() + + 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: AuthorizeClaim) => { + console.log("claim response:", claimData) + + + //swap code for token + let sub: string + let access_token: string + + // @ts-expect-error + const client: oauth.Client = { + client_id: connectData.client_id, + token_endpoint_auth_method: 'none', + } + + const as = { + issuer: `${authorizationUrl.protocol}//${authorizationUrl.host}`, + 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 as any), state) + if (Oauth.isOAuth2Error(params)) { + console.log('error', params) + throw new Error() // Handle OAuth 2.0 redirect error + } + console.log("ENDING--- Oauth.validateAuthResponse") + console.log("STARTING--- Oauth.authorizationCodeGrantRequest") + const response = await Oauth.authorizationCodeGrantRequest( + as, + client, + params, + connectData.redirect_uri, + codeVerifier, + ) + const payload = await response.json() + console.log("ENDING--- Oauth.authorizationCodeGrantRequest", payload) + + + //Create FHIR Client + const providerCredential: ProviderCredential = { + oauth_endpoint_base_url: connectData.oauth_endpoint_base_url, + api_endpoint_base_url: connectData.api_endpoint_base_url, + client_id: connectData.client_id, + redirect_uri: connectData.redirect_uri, + scopes: connectData.scopes.join(' '), + patient: payload.patient, + access_token: payload.access_token, + refresh_token: payload.refresh_token, + id_token: payload.id_token, + expires_at: getAccessTokenExpiration(payload, new BrowserAdapter()), + code_challenge: codeChallenge, + code_verifier: codeVerifier, + } + + this.fastenApi.createProviderCredential(providerCredential).subscribe( (respData) => { + console.log("provider credential create response:", respData) + }) + + + + + // console.log("STARTING--- FHIR.client(clientState)", clientState) + // const fhirClient = FHIR.client(clientState); + // + // console.log("STARTING--- client.request(Patient)") + // const patientResponse = await fhirClient.request("PatientAccess/v1/$userinfo") + // console.log(patientResponse) + + + + + + // // fetch userinfo response + // + // const response = await oauth.userInfoRequest(as, client, access_token) + // + // let challenges: oauth.WWWAuthenticateChallenge[] | undefined + // if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + // for (const challenge of challenges) { + // console.log('challenge', challenge) + // } + // throw new Error() // Handle www-authenticate challenges as needed + // } + // + // const result = await oauth.processUserInfoResponse(as, client, sub, response) + // console.log('result', result) + + + + }) + + }); + } + waitForClaimOrTimeout(providerId: string, state: string): Observable { + return this.passportApi.getProviderAuthorizeClaim(providerId, state).pipe( + retryWhen(error => + error.pipe( + concatMap((error, count) => { + if (count <= retryCount && error.status == 500) { + return of(error); + } + return throwError(error); + }), + delay(retryWaitMilliSeconds) + ) + ) + ) + } + + uuidV4(){ + // @ts-ignore + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); + } +} diff --git a/frontend/src/app/pages/user/user.component.html b/frontend/src/app/pages/patient/patient.component.html similarity index 100% rename from frontend/src/app/pages/user/user.component.html rename to frontend/src/app/pages/patient/patient.component.html diff --git a/frontend/src/app/pages/patient/patient.component.ts b/frontend/src/app/pages/patient/patient.component.ts new file mode 100644 index 00000000..7a4b5ebe --- /dev/null +++ b/frontend/src/app/pages/patient/patient.component.ts @@ -0,0 +1,11 @@ +import { Component, OnInit } from "@angular/core"; + +@Component({ + selector: "app-patient", + templateUrl: "patient.component.html" +}) +export class PatientComponent implements OnInit { + constructor() {} + + ngOnInit() {} +} diff --git a/frontend/src/app/pages/user/user.component.ts b/frontend/src/app/pages/user/user.component.ts deleted file mode 100644 index 35a83b5d..00000000 --- a/frontend/src/app/pages/user/user.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component, OnInit } from "@angular/core"; - -@Component({ - selector: "app-user", - templateUrl: "user.component.html" -}) -export class UserComponent implements OnInit { - constructor() {} - - ngOnInit() {} -}