From f2249b2df033ad81f29534061865b557c308f026 Mon Sep 17 00:00:00 2001 From: JAMES FUQIAN Date: Mon, 21 Nov 2022 21:30:52 -0800 Subject: [PATCH 1/6] 1st cut cleanup. --- README.md | 15 +-- server/Dockerfile | 3 - server/index.ts | 132 ++++++++++++++++++++ server/package.json | 71 ++--------- server/sample-bluebutton-config.json | 8 ++ server/spec/index.ts | 0 server/spec/loadEnv.ts | 10 -- server/spec/nodemon.json | 6 - server/src/@types/express/index.d.ts | 9 -- server/src/Server.ts | 46 ------- server/src/__tests__/server_test.ts | 104 --------------- server/src/configs/sample.config.ts | 42 ------- server/src/entities/AuthorizationToken.ts | 35 ------ server/src/entities/Settings.ts | 29 ----- server/src/index.ts | 13 -- server/src/pre-start/env/sandbox.sample.env | 14 --- server/src/pre-start/index.ts | 28 ----- server/src/routes/Authorize.ts | 110 ---------------- server/src/routes/Data.ts | 91 -------------- server/src/routes/Settings.ts | 12 -- server/src/routes/index.ts | 12 -- server/src/shared/Logger.ts | 13 -- server/src/utils/bb2.ts | 84 ------------- server/src/utils/db.ts | 66 ---------- server/src/utils/generatePKCE.ts | 29 ----- server/src/utils/request.ts | 109 ---------------- server/src/utils/user.ts | 18 --- server/tsconfig.json | 39 +++--- server/tsconfig.prod.json | 10 -- 29 files changed, 174 insertions(+), 984 deletions(-) create mode 100644 server/index.ts create mode 100644 server/sample-bluebutton-config.json delete mode 100644 server/spec/index.ts delete mode 100644 server/spec/loadEnv.ts delete mode 100644 server/spec/nodemon.json delete mode 100644 server/src/@types/express/index.d.ts delete mode 100644 server/src/Server.ts delete mode 100755 server/src/__tests__/server_test.ts delete mode 100644 server/src/configs/sample.config.ts delete mode 100644 server/src/entities/AuthorizationToken.ts delete mode 100644 server/src/entities/Settings.ts delete mode 100644 server/src/index.ts delete mode 100644 server/src/pre-start/env/sandbox.sample.env delete mode 100644 server/src/pre-start/index.ts delete mode 100644 server/src/routes/Authorize.ts delete mode 100644 server/src/routes/Data.ts delete mode 100644 server/src/routes/Settings.ts delete mode 100644 server/src/routes/index.ts delete mode 100644 server/src/shared/Logger.ts delete mode 100644 server/src/utils/bb2.ts delete mode 100644 server/src/utils/db.ts delete mode 100644 server/src/utils/generatePKCE.ts delete mode 100644 server/src/utils/request.ts delete mode 100644 server/src/utils/user.ts delete mode 100644 server/tsconfig.prod.json diff --git a/README.md b/README.md index 9de13fc..8f2d2b6 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,11 @@ Download and install node. Go to https://nodejs.org/en/download/ and follow the Once you have Docker and Node installed and setup then do the following: - copy server/src/configs/sample.config.ts -> server/src/configs/config.ts + cp server/sample-bluebutton-config.json -> server/.bluebutton-config.json Make sure to replace the clientId and clientSecret variables within the config file with the ones you were provided, for your application, when you created your Blue Button Sandbox account. - copy server/src/pre-start/env/sandbox.sample.env -> server/src/pre-start/env/development.env - docker-compose up -d This single command will create the docker container with all the necessary packages, configuration, and code to @@ -66,7 +64,7 @@ To start the sample in native OS (e.g. Linux) with server and client components 1. go to the base directory of the repo 2. run below to start the server: 1. yarn --cwd server install - 2. yarn --cwd server start:dev + 2. yarn --cwd server start 3. run below to start the client: 1. yarn --cwd client install 2. yarn --cwd client start-native @@ -77,15 +75,6 @@ Both ways of starting the sample are running the sample in foreground, logging a For client and server started separately in their command window, type Ctrl C respectively -## Run tests - -Go to local repo base directory: - -copy server/src/configs/sample.config.ts -> server/src/configs/config.ts - -yarn --cwd server install -yarn --cwd server test - ## Run selenium tests in docker Configure the remote target BB2 instance where the tested app is registered (as described above "Running the Back-end & Front-end") diff --git a/server/Dockerfile b/server/Dockerfile index da25f04..b42abca 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -5,9 +5,6 @@ LABEL description="Demo of a Medicare claims data sample app" WORKDIR /server -COPY ["./src/configs/sample.config.ts", "./src/configs/config.ts"] -COPY ["./src/pre-start/env/sandbox.sample.env","./src/pre-start/env/development.env"] - COPY . . RUN yarn install diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..37717f4 --- /dev/null +++ b/server/index.ts @@ -0,0 +1,132 @@ +import express, { Request, Response } from "express"; +import { AuthorizationToken, BlueButton } from "cms-bluebutton-sdk"; + +interface User { + authToken?: AuthorizationToken, + eobData?: any, + errors?: string[] +} + +const app = express(); + +const bb = new BlueButton(); +const authData = bb.generateAuthData(); + +// This is where medicare.gov beneficiary associated +// with the current logged in app user, +// in real app, this could be the app specific +// account management system + +const loggedInUser: User = { +}; + +// AuthorizationToken holds access grant info: +// access token, expire in, expire at, token type, scope, refreh token, etc. +// it is associated with current logged in user in real app, +// check SDK js docs for more details. + +let authToken: AuthorizationToken; + +// auth flow: response with URL to redirect to Medicare.gov beneficiary login +app.get("/api/authorize/authurl", (req: Request, res: Response) => { + res.send(bb.generateAuthorizeUrl(authData)); +}); + +// auth flow: oauth2 call back +app.get("/api/bluebutton/callback", async (req: Request, res: Response) => { + if (typeof req.query.error === "string") { + res.json({ message: req.query.error }); + } else { + if ( + typeof req.query.code === "string" && + typeof req.query.state === "string" + ) { + // let results; + try { + authToken = await bb.getAuthorizationToken( + authData, + req.query.code, + req.query.state + ); + // data flow: after access granted + // the app logic can fetch the beneficiary's data in app specific ways: + // e.g. download EOB periodically etc. + // access token can expire, SDK automatically refresh access token when that happens. + const eobResults = await bb.getExplanationOfBenefitData(authToken); + authToken = eobResults.token; // in case authToken got refreshed during fhir call + // const patientResults = await bb.getPatientData(authToken); + // authToken = patientResults.token; + // const coverageResults = await bb.getCoverageData(authToken); + // authToken = coverageResults.token; + // const profileResults = await bb.getProfileData(authToken); + // authToken = profileResults.token; + + // nav pages if needed for eob, patient, coverage + // client code can preemptively refresh tokens by calling refreshAuthToken(authToken) + // console.log( + // "============= preemptively do oauth token refresh before fetch EOB =================" + // ); + + // console.log("============= authToken ================="); + + // authToken = await bb.refreshAuthToken(authToken); + + // console.log(authToken); + + loggedInUser.authToken = authToken; + // console.log("============= EOB PAGES ================="); + + loggedInUser.eobData = eobResults.response?.data; + // const eobs = await bb.getPages(eobbundle, authToken); + // for (let i = 0; i < eobs.pages.length; i++) { + // fs.writeFileSync(`eob_p${i}.json`, JSON.stringify(eobs.pages[i])); + // } + + // authToken = eobs.token; + + // console.log("=============PATIENT================="); + // const ptbundle = patientResults.response?.data; + // const pts = await bb.getPages(ptbundle, authToken); + // authToken = pts.token; + + // console.log("=============COVERAGE================="); + // const coveragebundle = coverageResults.response?.data; + // const coverages = await bb.getPages(coveragebundle, authToken); + // authToken = coverages.token; + + // console.log("=============PROFILE================="); + // const pfbundle = profileResults.response?.data; + // const pfs = await bb.getPages(pfbundle, authToken); + // authToken = pfs.token; + + // results = { + // eob: eobs.pages, + // patient: pts.pages, + // coverage: coverages.pages, + // profile: pfs.pages, + // }; + } catch (e) { + console.log(e); + } + // res.json(results); + } else { + //res.json({ message: "Missing AC in callback." }); + console.log("Missing AC in callback."); + } + } + const fe_redirect_url = + process.env.SELENIUM_TESTS ? 'http://client:3000' : 'http://localhost:3000'; + res.redirect(fe_redirect_url); +}); + +// data flow: front end fetch eob +app.get("/api/data/benefit", async (req: Request, res: Response) => { + if (loggedInUser.eobData) { + res.json(loggedInUser.eobData); + } +}); + +const port = 3001; +app.listen(port, () => { + console.log(`[server]: Server is running at https://localhost:${port}`); +}); diff --git a/server/package.json b/server/package.json index 4732c64..b56545b 100644 --- a/server/package.json +++ b/server/package.json @@ -1,30 +1,13 @@ { "name": "server", - "version": "0.0.0", + "version": "1.0.0", + "description": "CMS Blue Button API Sample App", + "author": "CMS Blue Button API team", + "license": "MIT", "scripts": { - "build": "./node_modules/.bin/ts-node build.ts", "lint": "eslint --fix --ext .ts --ext .tsx .", - "start": "node -r module-alias/register ./dist --env=production", - "start:debug": "node --inspect=0.0.0.0:9229 ./node_modules/.bin/ts-node -r tsconfig-paths/register ./src", - "start:dev": "nodemon", - "test": "jest --coverage" - }, - "nodemonConfig": { - "watch": [ - "src" - ], - "ext": "ts, html", - "ignore": [ - "src/public" - ], - "exec": "./node_modules/.bin/ts-node -r tsconfig-paths/register ./src" - }, - "_moduleAliases": { - "@daos": "dist/daos", - "@entities": "dist/entities", - "@shared": "dist/shared", - "@configs": "dist/configs", - "@server": "dist/Server" + "start": "node ./node_modules/.bin/ts-node -r tsconfig-paths/register .", + "start:debug": "node --inspect=0.0.0.0:9229 ./node_modules/.bin/ts-node -r tsconfig-paths/register ." }, "eslintConfig": { "parser": "@typescript-eslint/parser", @@ -58,51 +41,23 @@ } }, "eslintIgnore": [ - "src/public/", "build.ts" ], "dependencies": { - "axios": "^0.21.2", - "command-line-args": "^5.1.1", - "cookie-parser": "^1.4.5", - "dotenv": "^8.2.0", - "express": "^4.17.1", - "express-async-errors": "^3.1.1", - "form-data": "^3.0.0", - "helmet": "^4.5.0", - "http-status-codes": "^2.1.4", - "jet-logger": "^1.0.4", - "jsonfile": "^6.1.0", - "module-alias": "^2.2.2", - "moment": "^2.29.1", - "morgan": "^1.10.0" + "@types/express": "^4.17.14", + "cms-bluebutton-sdk": "file:../../cms-bb2-node-sdk/", + "express": "^4.18.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.3" }, "devDependencies": { - "@babel/preset-env": "^7.16.11", - "@babel/preset-typescript": "^7.16.7", - "@types/command-line-args": "^5.0.0", - "@types/cookie-parser": "^1.4.2", - "@types/express": "^4.17.11", - "@types/find": "^0.2.1", - "@types/fs-extra": "^9.0.11", - "@types/jest": "^27.4.1", - "@types/jsonfile": "^6.0.0", - "@types/morgan": "^1.9.2", - "@types/node": "^15.0.1", - "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/parser": "^4.22.0", "eslint": "^7.25.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^16.1.0", "eslint-plugin-import": "^2.25.4", - "find": "^0.3.0", - "fs-extra": "^9.1.0", - "jest": "^27.5.1", - "nodemon": "^2.0.7", - "supertest": "^6.1.3", - "ts-node": "^10.8.1", - "tsconfig-paths": "^3.9.0", - "typescript": "^4.2.4" + "jest": "^29.3.1", + "tsconfig-paths": "^4.1.0" } } diff --git a/server/sample-bluebutton-config.json b/server/sample-bluebutton-config.json new file mode 100644 index 0000000..93305d9 --- /dev/null +++ b/server/sample-bluebutton-config.json @@ -0,0 +1,8 @@ +{ + "clientId": "", + "clientSecret": "", + "callbackUrl": "http://localhost:3001/api/bluebutton/callback/", + "version": "2", + "environment": "SANDBOX" +} + \ No newline at end of file diff --git a/server/spec/index.ts b/server/spec/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/server/spec/loadEnv.ts b/server/spec/loadEnv.ts deleted file mode 100644 index 33196e4..0000000 --- a/server/spec/loadEnv.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Set the env file, must be first -import dotenv from 'dotenv'; - -const result2 = dotenv.config({ - path: './src/pre-start/env/test.env', -}); - -if (result2.error) { - throw result2.error; -} diff --git a/server/spec/nodemon.json b/server/spec/nodemon.json deleted file mode 100644 index c2b0410..0000000 --- a/server/spec/nodemon.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "watch": ["spec"], - "ext": "spec.ts", - "ignore": ["spec/support"], - "exec": "./node_modules/.bin/ts-node -r tsconfig-paths/register ./spec" -} diff --git a/server/src/@types/express/index.d.ts b/server/src/@types/express/index.d.ts deleted file mode 100644 index 96288d2..0000000 --- a/server/src/@types/express/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IUser } from "@entities/User"; - -declare module 'express' { - export interface Request { - body: { - user: IUser - }; - } -} diff --git a/server/src/Server.ts b/server/src/Server.ts deleted file mode 100644 index 53f95d4..0000000 --- a/server/src/Server.ts +++ /dev/null @@ -1,46 +0,0 @@ -import cookieParser from 'cookie-parser'; -import morgan from 'morgan'; -import helmet from 'helmet'; - -import express, { Request, Response } from 'express'; -import StatusCodes from 'http-status-codes'; -import 'express-async-errors'; - -import BaseRouter from './routes'; -import logger from './shared/Logger'; - -const app = express(); -const { BAD_REQUEST } = StatusCodes; - -/** ********************************************************************************** - * Set basic express settings - ********************************************************************************** */ - -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); -app.use(cookieParser()); - -// Show routes called in console during development -if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'sandbox') { - app.use(morgan('dev')); -} - -// Security -if (process.env.NODE_ENV === 'production') { - app.use(helmet()); -} - -// Add APIs -app.use('/api', BaseRouter); - -// Print API errors -// eslint-disable-next-line @typescript-eslint/no-unused-vars -app.use((err: Error, _req: Request, res: Response) => { - logger.err(err, true); - return res.status(BAD_REQUEST).json({ - error: err.message, - }); -}); - -// Export express instance -export default app; diff --git a/server/src/__tests__/server_test.ts b/server/src/__tests__/server_test.ts deleted file mode 100755 index 3dae164..0000000 --- a/server/src/__tests__/server_test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import axios from "axios"; -import app from '../Server'; -import * as reqs from '../utils/request'; - -const BB2_BASE_URL = "https://sandbox.bluebutton.cms.gov"; - -const BB2_PATIENT_URL = `${BB2_BASE_URL}/v2/fhir/Patient/`; - -const BB2_COVERAGE_URL = `${BB2_BASE_URL}/v2/fhir/Coverage/`; - -const BB2_EOB_URL = `${BB2_BASE_URL}/v2/fhir/ExplanationOfBenefit/`; - -const BB2_PROFILE_URL = `${BB2_BASE_URL}/v2/connect/userinfo`; - -const eob = { status: 200, data: { resource: "EOB" } }; - -const coverage = { status: 200, data: { resource: "Coverage" } }; - -const patient = { status: 200, data: { resource: "Patient" } }; - -const profile = { status: 200, data: { resource: "Profile" } }; - -let server: any; - -beforeAll(() => { - server = app.listen(Number(3003)); - }); - -afterAll(() => { - server.close(); -}); - -test("expect patient end point returns patient data.", async () => { - jest.clearAllMocks(); - // mock patient returned at deeper layer - jest.spyOn(reqs, 'get').mockImplementation((url) => - { - if (url === BB2_PATIENT_URL) { - return Promise.resolve(patient); - } else { - throw Error("Invalid end point URL: " + url); - } - } - ); - - const response = await axios.get("http://localhost:3003/api/data/patient"); - - expect(response.status).toEqual(200); - expect(response.data).toEqual(patient.data); -}); - -test("expect profile end point returns profile data.", async () => { - jest.clearAllMocks(); - // mock profile returned at lower level get - jest.spyOn(reqs, 'get').mockImplementation((url) => - { - if (url === BB2_PROFILE_URL) { - return Promise.resolve(profile); - } else { - throw Error("Invalid end point URL: " + url); - } - }); - - const response = await axios.get("http://localhost:3003/api/data/userprofile"); - expect(response.status).toEqual(200); - expect(response.data).toEqual(profile.data); -}); - -test("expect coverage end point returns coverage data.", async () => { - jest.clearAllMocks(); - // mock coverage returned at deeper layer - jest.spyOn(reqs, 'get').mockImplementation((url) => - { - if (url === BB2_COVERAGE_URL) { - return Promise.resolve(coverage); - } else { - throw Error("Invalid end point URL: " + url); - } - } - ); - - const response = await axios.get("http://localhost:3003/api/data/coverage"); - - expect(response.status).toEqual(200); - expect(response.data).toEqual(coverage.data); -}); - -test("expect eob end point returns eob data.", async () => { - jest.clearAllMocks(); - // mock eob returned at lower level get - jest.spyOn(reqs, 'get').mockImplementation((url) => - { - if (url === BB2_EOB_URL) { - return Promise.resolve(eob); - } else { - throw Error("Invalid end point URL: " + url); - } - } - ); - - const response = await axios.get("http://localhost:3003/api/data/benefit-direct"); - expect(response.status).toEqual(200); - expect(response.data).toEqual(eob.data); -}); diff --git a/server/src/configs/sample.config.ts b/server/src/configs/sample.config.ts deleted file mode 100644 index e090ae5..0000000 --- a/server/src/configs/sample.config.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* -* DEVELOPER NOTES: -* Copy this file and rename it to config.js -* Replace your client/secret/callback url for each environment below with your specific app details -* (Note: local is mainly for BB2 internal developers) -*/ - -const SELENIUM_TESTS = process.env.SELENIUM_TESTS; - -export type ConfigType = { - [env: string]: { - bb2BaseUrl: string, - bb2ClientId: string, - bb2ClientSecret: string, - bb2CallbackUrl: string - } -}; - -const config: ConfigType = { - production: { - bb2BaseUrl: 'https://api.bluebutton.cms.gov', - bb2ClientId: '', - bb2ClientSecret: '', - bb2CallbackUrl: '', - }, - sandbox: { - bb2BaseUrl: 'https://sandbox.bluebutton.cms.gov', - bb2ClientId: '', - bb2ClientSecret: '', - bb2CallbackUrl: SELENIUM_TESTS ? - 'http://server:3001/api/bluebutton/callback/' - : 'http://localhost:3001/api/bluebutton/callback/', - }, - local: { - bb2BaseUrl: 'https://sandbox.bluebutton.cms.gov', - bb2ClientId: '', - bb2ClientSecret: '', - bb2CallbackUrl: 'http://localhost:3001/api/bluebutton/callback/', - }, -}; - -export default config; diff --git a/server/src/entities/AuthorizationToken.ts b/server/src/entities/AuthorizationToken.ts deleted file mode 100644 index 38b7959..0000000 --- a/server/src/entities/AuthorizationToken.ts +++ /dev/null @@ -1,35 +0,0 @@ -export interface IAuthorizationToken { - accessToken: string, - expiresIn: number, - tokenType: string, - scope: [string], - refreshToken: string, - patient: string, - expiresAt: number -} - -export default class AuthorizationToken implements IAuthorizationToken { - public accessToken: string; - - public expiresIn: number; - - public expiresAt: number; - - public tokenType: string; - - public scope: [string]; - - public refreshToken: string; - - public patient: string; - - constructor(authToken: any) { - this.accessToken = authToken.access_token; - this.expiresIn = authToken.expires_in; - this.expiresAt = authToken.expires_at; - this.patient = authToken.patient; - this.refreshToken = authToken.refresh_token; - this.scope = authToken.scope; - this.tokenType = authToken.token_type; - } -} diff --git a/server/src/entities/Settings.ts b/server/src/entities/Settings.ts deleted file mode 100644 index 367e42f..0000000 --- a/server/src/entities/Settings.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface ISettings { - env: string, - version: string, - pkce: boolean, -} - -export default class Settings implements ISettings { - public env: string; - - public version: string; - - public pkce: boolean; - - /* DEVELOPER NOTE: - * to utilize the latest security features/best practices - * it is recommended to utilize pkce - * - * Default values are hard coded here, but you may choose to store these values in a - * config file or other mechanism - * - * It's recommended that you use the latest version V2, which utilizes - * FHIR R4 - */ - constructor(settings: ISettings | undefined) { - this.env = settings?.env || 'sandbox'; - this.version = settings?.version || 'v2'; - this.pkce = settings?.pkce !== undefined ? settings.pkce : true; - } -} diff --git a/server/src/index.ts b/server/src/index.ts deleted file mode 100644 index bd8c2b0..0000000 --- a/server/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import './pre-start'; // Must be the first import -import app from './Server'; -import logger from './shared/Logger'; - -// Start the server -/* DEVELOPER NOTE: -* Default values are hard coded here, but you may choose to store these values in a -* config file or other mechanism -*/ -const port = Number(process.env.PORT || 3001); -app.listen(port, () => { - logger.info(`Express server started on port: ${port}`); -}); diff --git a/server/src/pre-start/env/sandbox.sample.env b/server/src/pre-start/env/sandbox.sample.env deleted file mode 100644 index ffc1d15..0000000 --- a/server/src/pre-start/env/sandbox.sample.env +++ /dev/null @@ -1,14 +0,0 @@ -## Environment ## -NODE_ENV=sandbox - - -## Server ## -PORT=3001 -HOST=localhost - - -## Setup jet-logger ## -JET_LOGGER_MODE=CONSOLE -JET_LOGGER_FILEPATH=jet-logger.log -JET_LOGGER_TIMESTAMP=TRUE -JET_LOGGER_FORMAT=LINE \ No newline at end of file diff --git a/server/src/pre-start/index.ts b/server/src/pre-start/index.ts deleted file mode 100644 index 24880a6..0000000 --- a/server/src/pre-start/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Pre-start is where we want to place things that must run BEFORE the express server is started. - * This is useful for environment variables, command-line arguments, and cron-jobs. - */ -import path from 'path'; -import dotenv from 'dotenv'; -import commandLineArgs from 'command-line-args'; - -(() => { - // Setup command line options - const options = commandLineArgs([ - { - name: 'env', - alias: 'e', - defaultValue: 'development', - type: String, - }, - ]); - // Set the env file - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const tail = `env/${options?.env || 'default'}.env`; - const result2 = dotenv.config({ - path: path.join(__dirname, tail), - }); - if (result2.error) { - throw result2.error; - } -})(); diff --git a/server/src/routes/Authorize.ts b/server/src/routes/Authorize.ts deleted file mode 100644 index 1a9c4a6..0000000 --- a/server/src/routes/Authorize.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Router, Request, Response } from 'express'; -import { clearBB2Data, getLoggedInUser } from '../utils/user'; -import logger from '../shared/Logger'; -import AuthorizationToken from '../entities/AuthorizationToken'; -import Settings from '../entities/Settings'; -import db from '../utils/db'; -import { getAccessToken, generateAuthorizeUrl } from '../utils/bb2'; -import { getBenefitReturnData } from './Data'; - -const BENE_DENIED_ACCESS = 'access_denied'; - -export async function authorizationCallback(req: Request, res: Response) { - try { - if (req.query.error === BENE_DENIED_ACCESS) { - const loggedInUser = getLoggedInUser(db); - // clear all saved claims data since the bene has denied access for the application - clearBB2Data(loggedInUser); - loggedInUser.errors.push(BENE_DENIED_ACCESS); - throw new Error('Beneficiary denied application access to their data'); - } - - if (!req.query.code) { - throw new Error('Response was missing access code'); - } - if (db.settings.pkce && !req.query.state) { - throw new Error('State is required when using PKCE'); - } - - // this gets the token from Medicare.gov once the 'user' - // authenticates their Medicare.gov account - const response = await getAccessToken(req.query.code?.toString(), req.query.state?.toString()); - - if (!response.data) { - throw new Error('Error get access token'); - } - - const loggedInUser = getLoggedInUser(db); - - if (response.status === 200) { - const authToken = new AuthorizationToken(response.data); - /* DEVELOPER NOTES: - * This is where you would most likely place some type of - * persistence service/functionality to store the token along with - * the application user identifiers - */ - - // Here we are grabbing the mocked 'user' for our application - // to be able to store the access token for that user - // thereby linking the 'user' of our sample applicaiton with their Medicare.gov account - // providing access to their Medicare data to our sample application - loggedInUser.authToken = authToken; - - /* DEVELOPER NOTES: - * Here we will use the token to get the EoB data for the mocked 'user' of the sample - * application then to save trips to the BB2 API we will store it in the mocked db - * with the mocked 'user' - * - * You could also request data for the Patient endpoint and/or the Coverage endpoint here - * using similar functionality - */ - const eobData = await getBenefitReturnData(req, res); - loggedInUser.eobData = eobData; - } else { - // send generic error message to FE - const general_err = '{"message": "Unable to load EOB Data - authorization failed."}'; - loggedInUser.eobData = JSON.parse(general_err); - } - } catch (e) { - /* DEVELOPER NOTES: - * This is where you could also use a data service or other exception handling - * to display or store the error - */ - logger.err(e); - } - /* DEVELOPER NOTE: - * This is a hardcoded redirect, but this should be used from settings stored in a conf file - * or other mechanism - */ - const fe_redirect_url = - process.env.SELENIUM_TESTS ? 'http://client:3000' : 'http://localhost:3000'; - res.redirect(fe_redirect_url); -} - -export function getAuthUrl(req: Request, res: Response) { - /* DEVELOPER NOTE: - * to utilize the latest security features/best practices - * it is recommended to utilize pkce - */ - const pkce = req.params.pkce === 'true'; - db.settings = new Settings({ - version: req.query?.version?.toString() || db.settings.version, - env: req.query?.env?.toString() || db.settings.env, - pkce: req.query?.pkce?.toString() ? pkce : db.settings.pkce, - }); - res.send(generateAuthorizeUrl()); -} - -export function getCurrentAuthToken(req: Request, res: Response) { - const loggedInUser = getLoggedInUser(db); - res.send(loggedInUser.authToken); -} - -const router = Router(); - -// eslint-disable-next-line @typescript-eslint/no-misused-promises -router.get('/bluebutton/callback', authorizationCallback); -router.get('/authorize/authurl', getAuthUrl); -router.get('/authorize/currentAuthToken', getCurrentAuthToken); - -export default router; diff --git a/server/src/routes/Data.ts b/server/src/routes/Data.ts deleted file mode 100644 index b6062e5..0000000 --- a/server/src/routes/Data.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Router, Request, Response } from 'express'; -import { getLoggedInUser } from '../utils/user'; -import config from '../configs/config'; -import db from '../utils/db'; -import { get } from '../utils/request'; - -const envConfig = config[db.settings.env]; - -function getURL(path: string): string { - return `${String(envConfig.bb2BaseUrl)}/${db.settings.version}/${path}`; -} - -/* DEVELOPER NOTES: -* This is our mocked Data Service layer for both the BB2 API -* as well as for our mocked db Service Layer -* we grouped them together for use of use for the front-end -*/ - -// this function is used to query eob data for the authenticated Medicare.gov -// user and returned - we are then storing in a mocked DB -export async function getBenefitReturnData(req: Request, res: Response) { - const loggedInUser = getLoggedInUser(db); - // get EOB end point - const response = await get(getURL('fhir/ExplanationOfBenefit/'), - req.query, - `${loggedInUser.authToken?.accessToken || 'no access token'}`); - return response.data as Record; -} - -// for bene-direct call -export async function getBenefitData(req: Request, res: Response) { - const eob_data = await getBenefitReturnData(req, res); - res.json(eob_data); -} - -/* -* DEVELOPER NOTES: -* this function is used directly by the front-end to -* retrieve eob data from the mocked DB -* This would be replaced by a persistence service layer for whatever -* DB you would choose to use -*/ -export function getBenefitDataEndPoint(req: Request, res: Response) { - const loggedInUser = getLoggedInUser(db); - const data = loggedInUser.eobData; - if (data) { - res.json(data); - } -} - -export async function getPatientData(req: Request, res: Response) { - const loggedInUser = getLoggedInUser(db); - // get Patient end point - const response = await get(getURL('fhir/Patient/'), - req.query, - `${loggedInUser.authToken?.accessToken || 'no access token'} `); - res.json(response.data); -} - -export async function getCoverageData(req: Request, res: Response) { - const loggedInUser = getLoggedInUser(db); - // get Coverage end point - const response = await get(getURL('fhir/Coverage/'), - req.query, - `${loggedInUser.authToken?.accessToken || 'no access token'}`); - res.json(response.data); -} - -export async function getUserProfileData(req: Request, res: Response) { - const loggedInUser = getLoggedInUser(db); - // get usrinfo end point - const response = await get(getURL('connect/userinfo'), - req.query, - `${loggedInUser.authToken?.accessToken || 'no access token'}`); - res.json(response.data); -} - -const router = Router(); - -router.get('/benefit', getBenefitDataEndPoint); -// turn off eslinting for below router get function - it's OK to call a async which return a promise -// eslint-disable-next-line @typescript-eslint/no-misused-promises -router.get('/benefit-direct', getBenefitData); -// eslint-disable-next-line @typescript-eslint/no-misused-promises -router.get('/patient', getPatientData); -// eslint-disable-next-line @typescript-eslint/no-misused-promises -router.get('/coverage', getCoverageData); -// eslint-disable-next-line @typescript-eslint/no-misused-promises -router.get('/userprofile', getUserProfileData); - -export default router; diff --git a/server/src/routes/Settings.ts b/server/src/routes/Settings.ts deleted file mode 100644 index 4b53a85..0000000 --- a/server/src/routes/Settings.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Router, Request, Response } from 'express'; -import db from '../utils/db'; - -export function getSettings(req: Request, res: Response) { - res.json(db.settings); -} - -const router = Router(); - -router.get('/', getSettings); - -export default router; diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts deleted file mode 100644 index e3f8e8f..0000000 --- a/server/src/routes/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Router } from 'express'; -import AuthorizeRouter from './Authorize'; -import SettingsRouter from './Settings'; -import DataRouter from './Data'; - -const baseRouter = Router(); - -baseRouter.use('/', AuthorizeRouter); -baseRouter.use('/settings', SettingsRouter); -baseRouter.use('/data', DataRouter); - -export default baseRouter; diff --git a/server/src/shared/Logger.ts b/server/src/shared/Logger.ts deleted file mode 100644 index dfef941..0000000 --- a/server/src/shared/Logger.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Setup the jet-logger. - * - * Documentation: https://github.com/seanpmaxwell/jet-logger - */ - -import Logger from 'jet-logger'; - -// throw error: src/shared/Logger.ts(9,20): error TS2351: This expression is not constructable. -// const logger = new Logger(); -const logger = Logger; - -export default logger; diff --git a/server/src/utils/bb2.ts b/server/src/utils/bb2.ts deleted file mode 100644 index 5c8f433..0000000 --- a/server/src/utils/bb2.ts +++ /dev/null @@ -1,84 +0,0 @@ -import FormData from 'form-data'; -import AuthorizationToken from '../entities/AuthorizationToken'; -import db from './db'; -import config from '../configs/config'; -import { generateCodeChallenge, generateRandomState } from './generatePKCE'; -import { post, postWithConfig } from './request'; - -const envConfig = config[db.settings.env]; - -function getURL(path: string): string { - return `${String(envConfig.bb2BaseUrl)}/${db.settings.version}/${path}`; -} - -function getClientId(): string { - return String(envConfig.bb2ClientId); -} - -function getClientSecret(): string { - return String(envConfig.bb2ClientSecret); -} - -function getCallbackUrl(): string { - return String(envConfig.bb2CallbackUrl); -} - -export function generateAuthorizeUrl(): string { - const BB2_AUTH_URL = getURL('o/authorize'); - - let pkceParams = ''; - const state = generateRandomState(); - - if (db.settings.pkce) { - const codeChallenge = generateCodeChallenge(); - pkceParams = `${'&code_challenge_method=S256' - + '&code_challenge='}${codeChallenge.codeChallenge}`; - - db.codeChallenges[state] = codeChallenge; - } - - return `${BB2_AUTH_URL - }?client_id=${getClientId() - }&redirect_uri=${getCallbackUrl() - }&state=${state - }&response_type=code${ - pkceParams}`; -} - -export async function getAccessToken(code: string, state: string | undefined) { - const BB2_ACCESS_TOKEN_URL = getURL('o/token/'); - - const form = new FormData(); - form.append('client_id', getClientId()); - form.append('client_secret', getClientSecret()); - form.append('code', code); - form.append('grant_type', 'authorization_code'); - form.append('redirect_uri', getCallbackUrl()); - - if (db.settings.pkce && state) { - const codeChallenge = db.codeChallenges[state]; - form.append('code_verifier', codeChallenge.verifier); - form.append('code_challenge', codeChallenge.codeChallenge); - } - return post(BB2_ACCESS_TOKEN_URL, form, form.getHeaders()); -} - -export async function refreshAccessToken(refreshToken: string) { - const BB2_ACCESS_TOKEN_URL = getURL('o/token/'); - - const tokenResponse = await postWithConfig({ - method: 'post', - url: BB2_ACCESS_TOKEN_URL, - auth: { - username: getClientId(), - password: getClientSecret(), - }, - params: { - grant_type: 'refresh_token', - client_id: getClientId(), - refresh_token: refreshToken, - }, - }); - - return new AuthorizationToken(tokenResponse.data); -} diff --git a/server/src/utils/db.ts b/server/src/utils/db.ts deleted file mode 100644 index b6f3b71..0000000 --- a/server/src/utils/db.ts +++ /dev/null @@ -1,66 +0,0 @@ -import AuthorizationToken from '../entities/AuthorizationToken'; -import Settings from '../entities/Settings'; -import { CodeChallenge } from './generatePKCE'; - -/* DEVELOPER NOTES: -* This is our mocked DB -*/ - -export interface UserInfo { - name: string, - userName: string, - pcp: string, - primaryFacility: string -} - -export interface User { - authToken?: AuthorizationToken, - userInfo: UserInfo, - eobData?: any, - errors: string[] -} -export interface DB { - patients: any, - users: User[], - codeChallenges: { - [key: string]: CodeChallenge - }, - codeChallenge: CodeChallenge, - settings: Settings -} - -const db: DB = { - patients: {}, - /* - * DEVELOPER NOTES: - * - * We are hard coding a Mock 'User' here of our demo application to save time - * creating/demoing a user logging into the application. - * - * This user will then need to linked to the Medicare.gov login - * to approve of having their medicare data accessed by the application - * these login's will be linked/related so anytime they login to the - * application, the application will be able to pull their medicare data. - * - * Just for ease of getting and displaying the data - * we are expecting this user to be linked to the - * BB2 Sandbox User BBUser29999 - */ - users: [{ - userInfo: { - name: 'John Doe', - userName: 'jdoe29999', - pcp: 'Dr. Hibbert', - primaryFacility: 'Springfield General Hospital', - }, - errors: [], - }], - codeChallenges: {}, - codeChallenge: { - codeChallenge: '', - verifier: '', - }, - settings: new Settings(undefined), -}; - -export default db; diff --git a/server/src/utils/generatePKCE.ts b/server/src/utils/generatePKCE.ts deleted file mode 100644 index 4bca7d4..0000000 --- a/server/src/utils/generatePKCE.ts +++ /dev/null @@ -1,29 +0,0 @@ -import crypto from 'crypto'; - -export type CodeChallenge = { - codeChallenge: string, - verifier: string -}; - -function base64URLEncode(buffer: Buffer): string { - return buffer.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); -} - -function sha256(str: string): Buffer { - return crypto.createHash('sha256').update(str).digest(); -} - -export function generateCodeChallenge(): CodeChallenge { - const verifier = base64URLEncode(crypto.randomBytes(32)); - return { - codeChallenge: base64URLEncode(sha256(verifier)), - verifier, - }; -} - -export function generateRandomState(): string { - return base64URLEncode(crypto.randomBytes(32)); -} diff --git a/server/src/utils/request.ts b/server/src/utils/request.ts deleted file mode 100644 index 2e07188..0000000 --- a/server/src/utils/request.ts +++ /dev/null @@ -1,109 +0,0 @@ -import axios from 'axios'; -import FormData from 'form-data'; -import logger from '../shared/Logger'; - -function sleep(time: number) { - return new Promise((resolve) => { - setTimeout(resolve, time); - }); -} - -function isRetryable(error: any) { - if (error.response && error.response.status === 500) { - /* eslint-disable-next-line @typescript-eslint/no-unsafe-call */ - if (error.request.path && error.request.path.match('^/v[12]/fhir/.*')) { - return true; - } - } - return false; -} - -// for demo: retry init-interval = 5 sec, max attempt 3, -// with retry interval = init-interval * (2 ** n) -// where n retry attempted -async function doRetry(config: any) { - const interval = 5; - const maxAttempts = 3; - let resp = null; - // retry order is important, need 'await' here - disable in loop and re-enable after loop - /* eslint-disable no-await-in-loop */ - for (let i = 0; i < maxAttempts; i += 1) { - const waitInSec = interval * (2 ** i); - logger.info(`wait ${waitInSec} seconds...`); - await sleep(waitInSec * 1000); - logger.info(`retry attempts: ${i + 1}`); - try { - resp = await axios(config); - logger.info('retry successful:'); - logger.info(resp.data); - break; - } catch (error: any) { - logger.info(`retry error: [${JSON.stringify(error.message)}]`); - if (error.response) { - logger.info(`response code: ${String(error.response.status)}`); - logger.info(`response data: ${JSON.stringify(error.response.data)}`); - resp = error.response; - } - } - } - /* eslint-enable no-await-in-loop */ - /* eslint-disable-next-line @typescript-eslint/no-unsafe-return */ - return resp; -} - -export async function request(config: any, retryFlag: boolean) { - let resp = null; - try { - resp = await axios(config); - } catch (error: any) { - // DEVELOPER NOTES: - // here handle errors per ErrorResponses.md - logger.info(`Error message: [${JSON.stringify(error.message)}]`); - if (error.response) { - logger.info(`response code: ${String(error.response?.status)}`); - logger.info(`response text: ${JSON.stringify(error.response.data)}`); - // DEVELOPER NOTES: - // check for retryable (e.g. 500 & fhir) errors and do retrying... - if (retryFlag && isRetryable(error)) { - logger.info('Request failed and is retryable, entering retry process...'); - const retryResp = await doRetry(config); - if (retryResp) { - resp = retryResp; - } - } else { - resp = error.response; - } - } else if (error.request) { - // something went wrong on sender side, not retryable - // error.request is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - logger.info(`error.request: ${String(error.request)}`); - } - } - /* eslint-disable-next-line @typescript-eslint/no-unsafe-return */ - return resp; -} - -export async function post(endpoint_url: string, data: FormData, headers: any) { - return request({ - method: 'post', - url: endpoint_url, - data, - headers, - }, true); -} - -export async function postWithConfig(config: any) { - return request(config, false); -} - -export async function get(endpointUrl: string, params: any, authToken: string) { - return request({ - method: 'get', - url: endpointUrl, - params, - headers: { - Authorization: `Bearer ${authToken}`, - }, - }, true); -} diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts deleted file mode 100644 index 55e2804..0000000 --- a/server/src/utils/user.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { DB, User } from './db'; - -/* DEVELOPER NOTES: -* Here we are literally just grabbing the first user -* in a mock list of users for the sample app -* This is where your app would have already authenticated the user -* and provided the details/data about that user to -* the other services/portions of the application -*/ -export function getLoggedInUser(db : DB) { - return db.users[0]; -} - -export function clearBB2Data(user: User) { - const userRef = user; - userRef.authToken = undefined; - userRef.eobData = undefined; -} diff --git a/server/tsconfig.json b/server/tsconfig.json index 759f841..b9ae5d6 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -59,29 +59,28 @@ /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ - "paths": { - "@daos/*": [ - "src/daos/*" - ], - "@entities/*": [ - "src/entities/*" - ], - "@shared/*": [ - "src/shared/*" - ], - "@configs/*": [ - "src/configs/*" - ], - "@server": [ - "src/Server" - ] - }, + // "paths": { + // "@daos/*": [ + // "src/daos/*" + // ], + // "@entities/*": [ + // "src/entities/*" + // ], + // "@shared/*": [ + // "src/shared/*" + // ], + // "@configs/*": [ + // "src/configs/*" + // ], + // "@server": [ + // "src/Server" + // ] + // }, }, "include": [ - "src/**/*.ts", - "spec/**/*.ts" + "index.ts" ], "exclude": [ - "src/public/", + // "src/public/", ] } diff --git a/server/tsconfig.prod.json b/server/tsconfig.prod.json deleted file mode 100644 index 3490cc5..0000000 --- a/server/tsconfig.prod.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "sourceMap": false - }, - "exclude": [ - "spec", - "src/**/*.mock.ts" - ] -} From f4013b6a6f6002b2c430f4251b116f3cfd57e79f Mon Sep 17 00:00:00 2001 From: JAMES FUQIAN Date: Wed, 23 Nov 2022 16:22:30 -0800 Subject: [PATCH 2/6] final touches. --- README-bb2-dev.md | 30 ++++++++++++++++++ server/cms-bluebutton-sdk-1.0.0.tgz | Bin 0 -> 78645 bytes server/package.json | 2 +- server/sample-bluebutton-selenium-config.json | 7 ++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 README-bb2-dev.md create mode 100644 server/cms-bluebutton-sdk-1.0.0.tgz create mode 100644 server/sample-bluebutton-selenium-config.json diff --git a/README-bb2-dev.md b/README-bb2-dev.md new file mode 100644 index 0000000..d977e84 --- /dev/null +++ b/README-bb2-dev.md @@ -0,0 +1,30 @@ +# Blue Button 2.0 Sample Application Development Documentation + +## Introduction + +This README contains information related to developing the SDK. + +It is intended for BB2 team members or others performing sample application development work. + +## Run selenium tests in docker + +Configure the remote target BB2 instance where the tested app is registered (as described above "Running the Back-end & Front-end") + +Change your `callbackUrl` configuration to use `server` instead of `localhost`. For example: +```JSON + "callback_url": "http://server:3001/api/bluebutton/callback/" +``` + +You will also need to add this URL to your `redirect_uris` list in your application's configuration on the BB2 Sandbox UI. + +Go to local repository base directory and run docker compose as below: + +docker-compose -f docker-compose.selenium.yml up --abort-on-container-exit + +Note: --abort-on-container-exit will abort client and server containers when selenium tests ends + +Note: You may need to clean up already existing Docker containers, if you are having issues or have changed your configuration file. + +## Visual trouble shoot + +Install VNC viewer and point browser to http://localhost:5900 to monitor web UI interactions \ No newline at end of file diff --git a/server/cms-bluebutton-sdk-1.0.0.tgz b/server/cms-bluebutton-sdk-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..3fa6cf857330140c6ba8f07aefce30983063fd41 GIT binary patch literal 78645 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∈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", + "clientSecret": "", + "callbackUrl": "http://server:3001/api/bluebutton/callback/", + "version": "2", + "environment": "SANDBOX" +} From 25617bbfbc961454ae62ea36125b8c4c4249f421 Mon Sep 17 00:00:00 2001 From: JAMES FUQIAN Date: Thu, 24 Nov 2022 16:11:45 -0800 Subject: [PATCH 3/6] fix selenium tests --- README.md | 18 ++---- selenium_tests/src/test_node_sample.py | 38 +++++++++++- server/index.ts | 83 ++++++++++---------------- 3 files changed, 69 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 8f2d2b6..400d9be 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ Once you have Docker and Node installed and setup then do the following: cp server/sample-bluebutton-config.json -> server/.bluebutton-config.json +or (if running a docker compose selenium tests) + + cp server/sample-bluebutton-selenium-config.json -> server/.bluebutton-config.json + Make sure to replace the clientId and clientSecret variables within the config file with the ones you were provided, for your application, when you created your Blue Button Sandbox account. @@ -75,20 +79,6 @@ Both ways of starting the sample are running the sample in foreground, logging a For client and server started separately in their command window, type Ctrl C respectively -## Run selenium tests in docker - -Configure the remote target BB2 instance where the tested app is registered (as described above "Running the Back-end & Front-end") - -Go to local repo base directory, from there run: - -docker-compose -f docker-compose.selenium.yml up --abort-on-container-exit - -Note: --abort-on-container-exit will abort client and server containers when selenium tests ends - -## Visual trouble shoot - -Install VNC viewer and point browser to http://localhost:5900 to monitor web UI interactions - ## Error Responses and handling: [See ErrorResponses.md](./ErrorResponses.md) diff --git a/selenium_tests/src/test_node_sample.py b/selenium_tests/src/test_node_sample.py index 0aade13..e7600e1 100644 --- a/selenium_tests/src/test_node_sample.py +++ b/selenium_tests/src/test_node_sample.py @@ -1,10 +1,11 @@ -# Generated by Selenium IDE import time from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait +CSS_SEL_FE_ERR_MSG_CONTENT = "tbody .ds-c-table__cell--align-left:nth-child(2)" + class TestNodeSampleApp(): driver_ready = False @@ -73,6 +74,13 @@ def _assert_EOB_table_records_present(self, cnt): elements = self._find_elem_xpath(xpath) assert len(elements) == cnt + def _assert_EOB_table_error_present(self): + element = self._find_and_return(30, + By.CSS_SELECTOR, + CSS_SEL_FE_ERR_MSG_CONTENT) + assert element is not None + print(element.text) + def _input_user_and_passwd_and_login(self): self._find_and_sendkey(30, By.ID, "username-textbox", "BBUser10000") self._find_and_sendkey(30, By.ID, "password-textbox", "PW10000!") @@ -85,6 +93,7 @@ def test_node_sample_app_grant_access(self): assert elem is not None self._input_user_and_passwd_and_login() self._find_and_click(30, By.ID, "approve") + time.sleep(5) self._assert_EOB_table_header_present() self._assert_EOB_table_records_present(10) @@ -97,6 +106,7 @@ def test_node_sample_app_grant_access_no_demographic(self): # select radio button "No Demographic Data" self._find_and_click(30, By.CSS_SELECTOR, "label:nth-child(5)") self._find_and_click(30, By.ID, "approve") + time.sleep(5) self._assert_EOB_table_header_present() self._assert_EOB_table_records_present(10) @@ -107,5 +117,27 @@ def test_node_sample_app_deny_access(self): assert elem is not None self._input_user_and_passwd_and_login() self._find_and_click(30, By.ID, "deny") - self._assert_EOB_table_header_present() - self._assert_EOB_table_records_present(0) + time.sleep(5) + self._assert_EOB_table_error_present() + + def test_node_sample_app_grant_followed_by_deny_access(self): + ''' + this is to verify that the cached result from previous + authorized eob query is clean up (should not see cached claims) + ''' + self.driver.get("http://client:3000/") + self.driver.set_window_size(1500, 1800) + elem = self._find_and_click(30, By.ID, "auth_btn") + assert elem is not None + self._input_user_and_passwd_and_login() + self._find_and_click(30, By.ID, "approve") + time.sleep(5) + self._assert_EOB_table_records_present(10) + # go again + elem = self._find_and_click(30, By.ID, "auth_btn") + assert elem is not None + self._input_user_and_passwd_and_login() + self._find_and_click(30, By.ID, "deny") + time.sleep(5) + # should see error message + self._assert_EOB_table_error_present() diff --git a/server/index.ts b/server/index.ts index 37717f4..1980877 100644 --- a/server/index.ts +++ b/server/index.ts @@ -7,6 +7,12 @@ interface User { errors?: string[] } +const BENE_DENIED_ACCESS = "access_denied" +const FE_MSG_ACCESS_DENIED = "Beneficiary denied app access to their data" +const ERR_QUERY_EOB = "Error when querying the patient's EOB!" +const ERR_MISSING_AUTH_CODE = "Response was missing access code!" +const ERR_MISSING_STATE = "State is required when using PKCE" + const app = express(); const bb = new BlueButton(); @@ -20,6 +26,12 @@ const authData = bb.generateAuthData(); const loggedInUser: User = { }; +// helper to clean up cached eob data +function clearBB2Data() { + loggedInUser.authToken = undefined; + loggedInUser.eobData = {}; +} + // AuthorizationToken holds access grant info: // access token, expire in, expire at, token type, scope, refreh token, etc. // it is associated with current logged in user in real app, @@ -35,13 +47,20 @@ app.get("/api/authorize/authurl", (req: Request, res: Response) => { // auth flow: oauth2 call back app.get("/api/bluebutton/callback", async (req: Request, res: Response) => { if (typeof req.query.error === "string") { - res.json({ message: req.query.error }); + // clear all cached claims eob data since the bene has denied access + // for the application + clearBB2Data(); + let errMsg = req.query.error; + if (req.query.error === BENE_DENIED_ACCESS) { + errMsg = FE_MSG_ACCESS_DENIED; + } + loggedInUser.eobData = {"message": errMsg}; + process.stdout.write(errMsg + '\n'); } else { if ( typeof req.query.code === "string" && typeof req.query.state === "string" ) { - // let results; try { authToken = await bb.getAuthorizationToken( authData, @@ -54,64 +73,22 @@ app.get("/api/bluebutton/callback", async (req: Request, res: Response) => { // access token can expire, SDK automatically refresh access token when that happens. const eobResults = await bb.getExplanationOfBenefitData(authToken); authToken = eobResults.token; // in case authToken got refreshed during fhir call - // const patientResults = await bb.getPatientData(authToken); - // authToken = patientResults.token; - // const coverageResults = await bb.getCoverageData(authToken); - // authToken = coverageResults.token; - // const profileResults = await bb.getProfileData(authToken); - // authToken = profileResults.token; - - // nav pages if needed for eob, patient, coverage - // client code can preemptively refresh tokens by calling refreshAuthToken(authToken) - // console.log( - // "============= preemptively do oauth token refresh before fetch EOB =================" - // ); - - // console.log("============= authToken ================="); - - // authToken = await bb.refreshAuthToken(authToken); - - // console.log(authToken); loggedInUser.authToken = authToken; - // console.log("============= EOB PAGES ================="); loggedInUser.eobData = eobResults.response?.data; - // const eobs = await bb.getPages(eobbundle, authToken); - // for (let i = 0; i < eobs.pages.length; i++) { - // fs.writeFileSync(`eob_p${i}.json`, JSON.stringify(eobs.pages[i])); - // } - - // authToken = eobs.token; - - // console.log("=============PATIENT================="); - // const ptbundle = patientResults.response?.data; - // const pts = await bb.getPages(ptbundle, authToken); - // authToken = pts.token; - - // console.log("=============COVERAGE================="); - // const coveragebundle = coverageResults.response?.data; - // const coverages = await bb.getPages(coveragebundle, authToken); - // authToken = coverages.token; - - // console.log("=============PROFILE================="); - // const pfbundle = profileResults.response?.data; - // const pfs = await bb.getPages(pfbundle, authToken); - // authToken = pfs.token; - - // results = { - // eob: eobs.pages, - // patient: pts.pages, - // coverage: coverages.pages, - // profile: pfs.pages, - // }; } catch (e) { - console.log(e); + loggedInUser.eobData = {}; + process.stdout.write(ERR_QUERY_EOB + '\n'); + process.stdout.write("Exception: " + e + '\n'); } - // res.json(results); } else { - //res.json({ message: "Missing AC in callback." }); - console.log("Missing AC in callback."); + clearBB2Data(); + process.stdout.write(ERR_MISSING_AUTH_CODE + '\n'); + process.stdout.write("OR" + '\n'); + process.stdout.write(ERR_MISSING_STATE + '\n'); + process.stdout.write("AUTH CODE: " + req.query.code + '\n'); + process.stdout.write("STATE: " + req.query.state + '\n'); } } const fe_redirect_url = From 4fd42244f9c277209d255385892c766631dc1505 Mon Sep 17 00:00:00 2001 From: James Fuqian Date: Fri, 25 Nov 2022 08:03:58 -0800 Subject: [PATCH 4/6] Update ci.yaml make CI pass to clear for PR --- .github/workflows/ci.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b90c8bf..9c0d0f7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,12 +21,3 @@ jobs: - name: Lint client source run: yarn --cwd client lint - - - name: Copy config - run: cp server/src/configs/sample.config.ts server/src/configs/config.ts - - - name: Copy .env - run: cp server/src/pre-start/env/sandbox.sample.env server/src/pre-start/env/development.env - - - name: Run server tests - run: yarn --cwd server test From 202383d23357e7083891e126afa7a3e7d6f59c8d Mon Sep 17 00:00:00 2001 From: JAMES FUQIAN Date: Fri, 25 Nov 2022 08:10:10 -0800 Subject: [PATCH 5/6] cleanup un-used artifacts and fix linting, sync with changed CI check workflow file. --- server/build.ts | 63 --------------------------- server/index.ts | 104 +++++++++++++++++++++++--------------------- server/package.json | 2 +- 3 files changed, 55 insertions(+), 114 deletions(-) delete mode 100644 server/build.ts diff --git a/server/build.ts b/server/build.ts deleted file mode 100644 index ee24868..0000000 --- a/server/build.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Remove old files, copy front-end ones. - */ - -import fs from 'fs-extra'; -import Logger from 'jet-logger'; -import childProcess from 'child_process'; - -// Setup logger -const logger = new Logger(); -logger.timestamp = false; - - - - -(async () => { - try { - // Remove current build - await remove('./dist/'); - // Copy front-end files - await copy('./src/public', './dist/public'); - await copy('./src/views', './dist/views'); - // Copy production env file - await copy('./src/pre-start/env/production.env', './dist/pre-start/env/production.env'); - // Copy back-end files - await exec('tsc --build tsconfig.prod.json', './') - } catch (err) { - logger.err(err); - } -})(); - - -function remove(loc: string): Promise { - return new Promise((res, rej) => { - return fs.remove(loc, (err) => { - return (!!err ? rej(err) : res()); - }); - }); -} - - -function copy(src: string, dest: string): Promise { - return new Promise((res, rej) => { - return fs.copy(src, dest, (err) => { - return (!!err ? rej(err) : res()); - }); - }); -} - - -function exec(cmd: string, loc: string): Promise { - return new Promise((res, rej) => { - return childProcess.exec(cmd, {cwd: loc}, (err, stdout, stderr) => { - if (!!stdout) { - logger.info(stdout); - } - if (!!stderr) { - logger.warn(stderr); - } - return (!!err ? rej(err) : res()); - }); - }); -} diff --git a/server/index.ts b/server/index.ts index 1980877..84479e4 100644 --- a/server/index.ts +++ b/server/index.ts @@ -45,59 +45,62 @@ app.get("/api/authorize/authurl", (req: Request, res: Response) => { }); // auth flow: oauth2 call back -app.get("/api/bluebutton/callback", async (req: Request, res: Response) => { - if (typeof req.query.error === "string") { - // clear all cached claims eob data since the bene has denied access - // for the application - clearBB2Data(); - let errMsg = req.query.error; - if (req.query.error === BENE_DENIED_ACCESS) { - errMsg = FE_MSG_ACCESS_DENIED; - } - loggedInUser.eobData = {"message": errMsg}; - process.stdout.write(errMsg + '\n'); - } else { - if ( - typeof req.query.code === "string" && - typeof req.query.state === "string" - ) { - try { - authToken = await bb.getAuthorizationToken( - authData, - req.query.code, - req.query.state - ); - // data flow: after access granted - // the app logic can fetch the beneficiary's data in app specific ways: - // e.g. download EOB periodically etc. - // access token can expire, SDK automatically refresh access token when that happens. - const eobResults = await bb.getExplanationOfBenefitData(authToken); - authToken = eobResults.token; // in case authToken got refreshed during fhir call - - loggedInUser.authToken = authToken; - - loggedInUser.eobData = eobResults.response?.data; - } catch (e) { - loggedInUser.eobData = {}; - process.stdout.write(ERR_QUERY_EOB + '\n'); - process.stdout.write("Exception: " + e + '\n'); +app.get("/api/bluebutton/callback", (req: Request, res: Response) => { + (async (req: Request, res: Response) => { + if (typeof req.query.error === "string") { + // clear all cached claims eob data since the bene has denied access + // for the application + clearBB2Data(); + let errMsg = req.query.error; + if (req.query.error === BENE_DENIED_ACCESS) { + errMsg = FE_MSG_ACCESS_DENIED; + } + loggedInUser.eobData = {"message": errMsg}; + process.stdout.write(errMsg + '\n'); + } else { + if ( + typeof req.query.code === "string" && + typeof req.query.state === "string" + ) { + try { + authToken = await bb.getAuthorizationToken( + authData, + req.query.code, + req.query.state + ); + // data flow: after access granted + // the app logic can fetch the beneficiary's data in app specific ways: + // e.g. download EOB periodically etc. + // access token can expire, SDK automatically refresh access token when that happens. + const eobResults = await bb.getExplanationOfBenefitData(authToken); + authToken = eobResults.token; // in case authToken got refreshed during fhir call + + loggedInUser.authToken = authToken; + + loggedInUser.eobData = eobResults.response?.data; + } catch (e) { + loggedInUser.eobData = {}; + process.stdout.write(ERR_QUERY_EOB + '\n'); + process.stdout.write("Exception: " + e + '\n'); + } + } else { + clearBB2Data(); + process.stdout.write(ERR_MISSING_AUTH_CODE + '\n'); + process.stdout.write("OR" + '\n'); + process.stdout.write(ERR_MISSING_STATE + '\n'); + process.stdout.write("AUTH CODE: " + req.query.code + '\n'); + process.stdout.write("STATE: " + req.query.state + '\n'); + } + } + const fe_redirect_url = + process.env.SELENIUM_TESTS ? 'http://client:3000' : 'http://localhost:3000'; + res.redirect(fe_redirect_url); } - } else { - clearBB2Data(); - process.stdout.write(ERR_MISSING_AUTH_CODE + '\n'); - process.stdout.write("OR" + '\n'); - process.stdout.write(ERR_MISSING_STATE + '\n'); - process.stdout.write("AUTH CODE: " + req.query.code + '\n'); - process.stdout.write("STATE: " + req.query.state + '\n'); - } - } - const fe_redirect_url = - process.env.SELENIUM_TESTS ? 'http://client:3000' : 'http://localhost:3000'; - res.redirect(fe_redirect_url); + )(req, res); }); // data flow: front end fetch eob -app.get("/api/data/benefit", async (req: Request, res: Response) => { +app.get("/api/data/benefit", (req: Request, res: Response) => { if (loggedInUser.eobData) { res.json(loggedInUser.eobData); } @@ -105,5 +108,6 @@ app.get("/api/data/benefit", async (req: Request, res: Response) => { const port = 3001; app.listen(port, () => { - console.log(`[server]: Server is running at https://localhost:${port}`); + process.stdout.write(`[server]: Server is running at https://localhost:${port}`); + process.stdout.write("\n"); }); diff --git a/server/package.json b/server/package.json index 39e98e2..783bd0c 100644 --- a/server/package.json +++ b/server/package.json @@ -5,7 +5,7 @@ "author": "CMS Blue Button API team", "license": "MIT", "scripts": { - "lint": "eslint --fix --ext .ts --ext .tsx .", + "lint": "eslint --fix --ext .ts --ext .tsx index.ts", "start": "node ./node_modules/.bin/ts-node -r tsconfig-paths/register .", "start:debug": "node --inspect=0.0.0.0:9229 ./node_modules/.bin/ts-node -r tsconfig-paths/register ." }, From 3149ba67fb29cc0a97f3b2af0d9f6845281408cf Mon Sep 17 00:00:00 2001 From: JAMES FUQIAN Date: Fri, 2 Dec 2022 14:05:35 -0800 Subject: [PATCH 6/6] final touches on readme files. --- README-bb2-dev.md | 4 ++++ README.md | 6 +----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README-bb2-dev.md b/README-bb2-dev.md index d977e84..ccbf558 100644 --- a/README-bb2-dev.md +++ b/README-bb2-dev.md @@ -15,6 +15,10 @@ Change your `callbackUrl` configuration to use `server` instead of `localhost`. "callback_url": "http://server:3001/api/bluebutton/callback/" ``` +You can also start your configuration by the following the sample config template for selenium tests: + +cp server/sample-bluebutton-selenium-config.json server/.bluebutton-config.json + You will also need to add this URL to your `redirect_uris` list in your application's configuration on the BB2 Sandbox UI. Go to local repository base directory and run docker compose as below: diff --git a/README.md b/README.md index 400d9be..17f388e 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,7 @@ Download and install node. Go to https://nodejs.org/en/download/ and follow the Once you have Docker and Node installed and setup then do the following: - cp server/sample-bluebutton-config.json -> server/.bluebutton-config.json - -or (if running a docker compose selenium tests) - - cp server/sample-bluebutton-selenium-config.json -> server/.bluebutton-config.json + cp server/sample-bluebutton-config.json server/.bluebutton-config.json Make sure to replace the clientId and clientSecret variables within the config file with the ones you were provided, for your application, when you created your Blue Button Sandbox account.