Add SMART on FHIR config screen

This commit is contained in:
Josh Mandel 2020-05-06 12:10:00 -05:00
Родитель 792794ff7b
Коммит cc2214388b
7 изменённых файлов: 158 добавлений и 42 удалений

Просмотреть файл

@ -2,16 +2,20 @@
## Verifiable Credential (VC) Types
* https://healthwallet.cards#covid19: A VC designed to convey COVID-19 details
* https://healthwallet.cards#presentation-context-online: A VC designed for online presentation
* https://healthwallet.cards#presentation-context-in-person A VC designed for in-person presentation
* `https://healthwallet.cards#covid19`: A VC designed to convey COVID-19 details
* `https://healthwallet.cards#immunization`: A VC designed to convey any immunization details
* `https://healthwallet.cards#presentation-context-online`: A VC designed for online presentation
* `https://healthwallet.cards#presentation-context-in-person`: A VC designed for in-person presentation
## FHIR Extensions
* https://healthwallet.cards#vc-attachment: Extension that decorates a FHIR "key resource" to attach a VC
* `https://healthwallet.cards#vc-attachment`: Extension that decorates a FHIR "key resource" to attach a VC
## FHIR Codings
The following `code`s are defined in the `https://healthwallet.cards` system:
The following codes are defined in the `https://healthwallet.cards` system, for use in tagging a FHIR "key resource" (in `.meta.tag`) as containing a specific type of VC. This facilitates search across FHIR resources to find resources with attached VCs.
* `covid19`: Used for tagging a FHIR "key resource" (in `.meta.tag`) as containing a VC of type https://healthwallet.cards#covid19. This can facilitate search across FHIR resources to find resources with attached VCs.
* `covid19`: Used for tagging a FHIR "key resource" as containing a VC of type `https://healthwallet.cards#covid19`
* `immunization`: Used for tagging a FHIR "key resource" as containing a VC of type `https://healthwallet.cards#immunization`

Просмотреть файл

@ -1,5 +1,5 @@
import { EncryptionKey, SigningKey, KeyGenerators } from './KeyTypes';
export type ClaimType = 'vc-health-passport-stamp-covid19-serology' | 'vc-health-passport-stamp';
export type ClaimType = 'https://healthwallet.cards#covid19' | 'https://healthwallet.cards#immunization';
export type SiopResponseMode = 'form_post' | 'fragment';
export interface VerifierState {

Просмотреть файл

@ -4,4 +4,4 @@ if (serverBase === 'relative') {
}
export const resolveUrl = `${serverBase}/did/`;
console.log('SERVER base', process.env, process.env.SERVER_BASE);
console.log('SERVER base', process.env, process.env.SERVER_BASE, serverBase);

Просмотреть файл

@ -1,17 +1,19 @@
import axios from 'axios';
import base64url from 'base64url';
import 'bootstrap/dist/css/bootstrap.min.css';
import './style.css';
import * as crypto from 'crypto';
import QrScanner from 'qr-scanner';
import qs from 'querystring';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState, useReducer } from 'react';
import ReactDOM from 'react-dom';
import * as RS from 'reactstrap';
import { Button, Card, CardSubtitle, CardText, CardTitle, Collapse, Nav, NavbarBrand, NavbarText, NavbarToggler, NavLink } from 'reactstrap';
import { Button, Card, CardSubtitle, CardText, CardTitle, Collapse, Nav, NavbarBrand, NavbarText, NavbarToggler, NavLink, InputGroupAddon, InputGroupText, Dropdown, DropdownMenu, DropdownItem, DropdownToggle } from 'reactstrap';
import { holderReducer, HolderState, initializeHolder, prepareSiopResponse, receiveSiopRequest, retrieveVcs, SiopInteraction } from './holder';
import { issuerWorld } from './issuer';
import { verifierWorld } from './verifier';
import { ClaimType } from './VerifierState';
import * as config from './config';
QrScanner.WORKER_PATH = 'qr-scanner-worker.min.js';
@ -106,11 +108,11 @@ const SiopApprovalModal: React.FC<SiopApprovalProps | null> = (props) => {
<RS.ModalBody>The following details will be shared:
<ul>
<li><b>Your ID Card:</b> allows secure communications</li>
{props.share.includes("vc-health-passport-stamp-covid19-serology") && <li>
{props.share.includes('https://healthwallet.cards#covid19') && <li>
<b>Your COVID Card:</b> labs and vaccines for COVID-19
</li>
}
{props.share.includes("vc-health-passport-stamp") && <li>
{props.share.includes('https://healthwallet.cards#immunization') && <li>
<b>Your Immunizations Card:</b> full vaccine history
</li>
}
@ -128,6 +130,59 @@ const SiopApprovalModal: React.FC<SiopApprovalProps | null> = (props) => {
</>
}
const ConfigEditOption: React.FC<{ title: string, default: string, value: string, onChange: (string) => void }> = (props) => {
return <>
<RS.InputGroup>
<InputGroupAddon addonType='prepend' className='config-prepend'>
<RS.InputGroupText>{props.title}</RS.InputGroupText>
</InputGroupAddon>
<RS.Input type="text"
value={props.value}
onChange={e => props.onChange(e.target.value)}>
</RS.Input>
<InputGroupAddon addonType="prepend">
<Button onClick={e => props.onChange(props.default)}></Button>
</InputGroupAddon>
</RS.InputGroup>
<br />
</>
}
const ConfigEditModal: React.FC<{ uiState: UiState, onSave: any, onDiscard: any }> = (props) => {
const [ui, setUi] = useState(props.uiState)
return <>
<RS.Modal isOpen={true} >
<RS.ModalHeader>Edit Config Settings</RS.ModalHeader>
<RS.ModalBody>
<ConfigEditOption title="FHIR Server"
default={props.uiState.fhirClient.server}
value={ui.fhirClient.server}
onChange={v => setUi({ ...ui, fhirClient: { ...ui.fhirClient, server: v } })} />
<ConfigEditOption title="Client ID"
default={props.uiState.fhirClient.client_id}
value={ui.fhirClient.client_id}
onChange={v => setUi({ ...ui, fhirClient: { ...ui.fhirClient, client_id: v } })} />
<ConfigEditOption title="Client Secret"
default={props.uiState.fhirClient.client_secret}
value={ui.fhirClient.client_secret}
onChange={v => setUi({ ...ui, fhirClient: { ...ui.fhirClient, client_secret: v } })} />
<ConfigEditOption title="Scopes"
default={props.uiState.fhirClient.scope}
value={ui.fhirClient.scope}
onChange={v => setUi({ ...ui, fhirClient: { ...ui.fhirClient, scope: v } })} />
</RS.ModalBody>
<RS.ModalFooter>
<Button color="danger" onClick={e => props.onDiscard()}>Cancel</Button>
<Button color="success" onClick={e => props.onSave(ui)}>Save to Bookmarkable URL</Button>
</RS.ModalFooter>
</RS.Modal>
</>
}
interface IssuerProps {
issuerStartUrl: string;
issuerDownloadUrl: string;
@ -149,15 +204,41 @@ interface SmartState {
server: string;
}
interface AppProps {
initialState: HolderState;
simulatedBarcodeScan: boolean;
issuer: IssuerProps;
verifier: VerifierProps;
oauth: OAuthProps;
interface UiState {
issuer: IssuerProps,
verifier: VerifierProps,
fhirClient: OAuthProps,
editingConfig: boolean
}
interface AppProps {
initialHolderState: HolderState;
simulatedBarcodeScan: boolean;
initialUiState: UiState
}
type UiEvent = { type: 'save-ui-state', newState: UiState } | { type: 'toggle-editing-config' }
const uiReducer = (prevState: UiState, action: UiEvent): UiState => {
if (action.type === 'save-ui-state') {
return {
...action.newState,
editingConfig: false
}
}
if (action.type === 'toggle-editing-config') {
return {
...prevState,
editingConfig: !prevState.editingConfig
}
}
}
const App: React.FC<AppProps> = (props) => {
const [holderState, setHolderState] = useState<HolderState>(props.initialState)
const [holderState, setHolderState] = useState<HolderState>(props.initialHolderState)
const [uiState, dispatch] = useReducer(uiReducer, props.initialUiState)
const [smartState, setSmartState] = useState<SmartState | null>(null)
@ -216,7 +297,7 @@ const App: React.FC<AppProps> = (props) => {
await dispatchToHolder(retrieveVcs(vcs, holderState))
}
window.addEventListener("message", onMessage)
window.open(props.issuer.issuerDownloadUrl)
window.open(uiState.issuer.issuerDownloadUrl)
}
const onScanned = async (qrCodeUrl: string) => {
@ -234,10 +315,10 @@ const App: React.FC<AppProps> = (props) => {
const fhirConnect = async () => {
const state = base64url.encode(crypto.randomBytes(32))
const server = props.oauth?.server || './api/fhir'
const client_id = props.oauth?.client_id || 'sample_client_id'
const client_secret = props.oauth?.client_secret || 'sample_client_secret'
const scope = props.oauth?.scope || 'launch launch/patient patient/*.*'
const server = uiState.fhirClient?.server || './api/fhir'
const client_id = uiState.fhirClient?.client_id || 'sample_client_id'
const client_secret = uiState.fhirClient?.client_secret || 'sample_client_secret'
const scope = uiState.fhirClient?.scope || 'launch launch/patient patient/*.*'
const redirect_uri = window.location.origin + window.location.pathname + 'authorized.html'
const smartConfig = (await axios.get(server + '/.well-known/smart-configuration.json')).data
@ -286,6 +367,7 @@ const App: React.FC<AppProps> = (props) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
let currentStep = 1;
/* tslint:disable-next-line:prefer-conditional-expression */
if (issuerInteraction?.status !== 'complete') {
@ -309,8 +391,8 @@ const App: React.FC<AppProps> = (props) => {
<NavbarToggler onClick={toggle} />
<Collapse navbar={true} isOpen={isOpen}>
<Nav navbar={true}>
<NavLink href="#" onClick={fhirConnect}> Connect to Lab via FHIR API</NavLink>
<NavLink href="#" onClick={connectTo('verifier')}> Open Employer Portal</NavLink>
<NavLink href="#config" onClick={e => dispatch({ type: 'toggle-editing-config' })}> Edit Config</NavLink>
<NavLink target="_blank" href="https://github.com/microsoft-healthcare-madison/health-wallet-demo">Source on GitHub</NavLink>
</Nav>
</Collapse></RS.Container>
@ -321,8 +403,19 @@ const App: React.FC<AppProps> = (props) => {
redirectMode="window-open"
label={siopAtNeedQr[0].siopPartnerRole}
startUrl={siopAtNeedQr[0].siopPartnerRole === 'issuer' ?
props.issuer.issuerStartUrl : props.verifier.verifierStartUrl}
uiState.issuer.issuerStartUrl : uiState.verifier.verifierStartUrl}
interaction={siopAtNeedQr[0]} />}
{uiState.editingConfig && <ConfigEditModal uiState={uiState}
onSave={ui => {
window.location.hash = JSON.stringify({ ...ui, editingConfig: undefined }, null, 0)
dispatch({ type: 'save-ui-state', newState: ui })
}
}
onDiscard={() => {
dispatch({ type: 'toggle-editing-config' });
}}
/>}
<SiopApprovalModal {...parseSiopApprovalProps(holderState, onApproval, onDenial)} />
<RS.Container >
@ -342,13 +435,9 @@ const App: React.FC<AppProps> = (props) => {
</CardTitle>
<CardSubtitle className="text-muted">Your COVID results are ready to share</CardSubtitle>
<CardText style={{ fontFamily: "monospace" }}>
<div>
{holderState.vcStore[0].vcSigned.slice(0, 25)}...
</div>
<div>
<span>
{JSON.stringify(holderState.vcStore[0].vcPayload, null).slice(0, 100)}...
</div>
</span>
</CardText>
</Card>}
@ -361,9 +450,17 @@ const App: React.FC<AppProps> = (props) => {
<Button disabled={true} className="mb-1" color="info">
{currentStep > 1 && '✓ '} 1. Set up your Health Wallet</Button>
<Button disabled={currentStep !== 2} onClick={connectTo('issuer')} className="mb-1" color={currentStep === 2 ? 'success' : 'info'}>
{currentStep > 2 && '✓ '}
2. Find a lab and get tested</Button>
<RS.UncontrolledButtonDropdown className="mb-1" >
<DropdownToggle caret color={currentStep === 2 ? 'success' : 'info'} >
Connect to Lab and get tested
</DropdownToggle>
<DropdownMenu style={{width: "100%"}}>
<DropdownItem onClick={fhirConnect} >Connect with SMART on FHIR </DropdownItem>
<DropdownItem onClick={connectTo('issuer')} >Start from Lab Portal</DropdownItem>
</DropdownMenu>
</RS.UncontrolledButtonDropdown>
<Button disabled={currentStep !== 3} onClick={retrieveVcClick} className="mb-1" color={currentStep === 3 ? 'success' : 'info'} >
{currentStep > 3 && '✓ '}
3. Save COVID card to wallet</Button>
@ -405,13 +502,20 @@ export default async function main() {
const verifierStartUrl = queryProps.verifierStartUrl as string || `./verifier.html?begin`
console.log("issuersta", issuerStartUrl)
const server = config.serverBase + '/fhir'
const client_id = 'sample_client_id'
const client_secret = 'sample_client_secret'
const scope = 'launch launch/patient patient/*.*'
const redirect_uri = window.location.origin + window.location.pathname + 'authorized.html'
const defaultUiState = JSON.parse(decodeURIComponent(window.location.hash.slice(1))) as UiState
ReactDOM.render(
<App initialState={state}
<App initialHolderState={state}
simulatedBarcodeScan={simulatedBarcodeScan}
verifier={{ verifierStartUrl }}
issuer={{ issuerStartUrl, issuerDownloadUrl }}
oauth={{}} />,
document.getElementById('app')
initialUiState={defaultUiState}
/>, document.getElementById('app')
);
}

Просмотреть файл

@ -168,7 +168,7 @@ export async function holderReducer(state: HolderState, event: any): Promise<Hol
return {
...state,
vcStore: [...state.vcStore, {
type: "vc-health-passport-stamp-covid19-serology", // TODO inspect VC for type
type: 'https://healthwallet.cards#covid19', // TODO inspect VC for type
vcSigned: event.vc,
vcPayload: event.vcPayload
}]

Просмотреть файл

@ -12,7 +12,7 @@ import { verifierReducer, prepareSiopRequest, parseSiopResponse } from './Verifi
export async function verifierWorld (role = 'verifier', responseMode: SiopResponseMode = 'form_post', reset = false) {
let state = await initializeVerifier({
role,
claimsRequired: ['vc-health-passport-stamp-covid19-serology'],
claimsRequired: ['https://healthwallet.cards#covid19'],
responseMode: responseMode,
reset,
displayQr: false,

Просмотреть файл

@ -71,4 +71,12 @@ describe('CredentialManager', () => {
})
test('prints', ()=>{
const x = require('../src/fixtures/vc-payload.json')
const asVc = CredentialManager.vcToJwtPayload(x)
console.log(JSON.stringify(asVc, null, 2))
})
});