Add SMART on FHIR config screen
This commit is contained in:
Родитель
792794ff7b
Коммит
cc2214388b
|
@ -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))
|
||||
})
|
||||
|
||||
|
||||
});
|
Загрузка…
Ссылка в новой задаче