Skip to content

Commit

Permalink
Proposed CDS Hooks launch support
Browse files Browse the repository at this point in the history
  • Loading branch information
vlad-ignatov committed May 28, 2024
1 parent 3c79b50 commit 703910c
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 27 deletions.
3 changes: 2 additions & 1 deletion backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ app.use("/env.js", (_, res) => {
FHIR_SERVER_R4 : config.fhirServerR4,
ACCESS_TOKEN : jwt.sign({ client_id: "launcherUI" }, config.jwtSecret, { expiresIn: "10 years" }),
VERSION : pkg.version,
COMMIT : process.env.SOURCE_VERSION
COMMIT : process.env.SOURCE_VERSION,
CDS_SANDBOX_URL : process.env.CDS_SANDBOX_URL
};

res.type("application/javascript").send(`var ENV = ${JSON.stringify(out, null, 4)};`);
Expand Down
6 changes: 3 additions & 3 deletions backend/routes/auth/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ export default class AuthorizeHandler {
return scope.has("launch/patient") || scope.has("launch");
}

// if (launch_type === "cds-hooks") {
// return scope.has("launch/patient") || scope.has("launch");
// }
if (launch_type === "cds-hooks") {
return scope.has("launch/patient") || scope.has("launch");
}

return false
}
Expand Down
21 changes: 20 additions & 1 deletion backend/routes/fhir/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Router, text } from "express"
import { Router, json, text } from "express"
import getWellKnownSmartConfig from "./.well-known/smart-configuration"
import getWellKnownOpenidConfig from "./.well-known/openid-configuration"
import getCapabilityStatement from "./metadata"
Expand All @@ -11,6 +11,25 @@ const router = Router({ mergeParams: true })
router.get("/.well-known/smart-configuration" , getWellKnownSmartConfig)
router.get("/.well-known/openid-configuration", getWellKnownOpenidConfig)
router.get("/metadata", asyncRouteWrap(getCapabilityStatement))


// Provide launch_id if the CDS Sandbox asks for it
router.post("/_services/smart/Launch", json(), (req, res) => {
// {
// "launchUrl":"https://examples.smarthealthit.org/growth-chart-app/launch.html",
// "parameters":{
// "patient":"2e27c71e-30c8-4ceb-8c1c-5641e066c0a4",
// "smart_messaging_origin":"https://sandbox.cds-hooks.org",
// "appContext":"{\"patient\":\"099e7de7-c952-40e2-9b4e-0face78c9d80\",\"encounter\": \"1d3f33a3-5e0b-4508-8836-ecabcab2ff4c\"}"
// }
// }
res.json({
launch_id: Buffer.from(JSON.stringify({
context: req.body.parameters || {}
}), "utf8").toString("base64")
});
});

router.use("/", text({ type: "*/*", limit: 1e6 }), asyncRouteWrap(fhirProxy))

export default router
3 changes: 2 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ declare global {
ACCESS_TOKEN : string
VERSION : string
COMMIT : string
CDS_SANDBOX_URL : string
}
}

Expand All @@ -19,7 +20,7 @@ declare namespace SMART {
/**
* All the launch types that we recognize
*/
type LaunchType = "provider-ehr" | "patient-portal" | "provider-standalone" | "patient-standalone" | "backend-service"; //| "cds-hooks";
type LaunchType = "provider-ehr" | "patient-portal" | "provider-standalone" | "patient-standalone" | "backend-service" | "cds-hooks";

type SimulatedError =

Expand Down
68 changes: 48 additions & 20 deletions src/components/Launcher/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ const launchTypes = [
}
];

if (window.ENV.CDS_SANDBOX_URL) {
launchTypes.push({
name : "CDS Hooks Service",
description: "Test your CDS services",
value : "cds-hooks"
})
}

const DEFAULT_LAUNCH_PARAMS: SMART.LaunchParams = {
launch_type : "provider-ehr",
patient : "",
Expand Down Expand Up @@ -122,6 +130,7 @@ export default function Launcher() {
const isStandaloneLaunch = launch_type.includes("standalone");

const isBackendService = launch_type === "backend-service";
const isCDSHooksLaunch = launch_type === "cds-hooks";

const { origin } = window.location;

Expand Down Expand Up @@ -172,8 +181,14 @@ export default function Launcher() {
userLaunchUrl = new URL(`/ehr?app=${encodeURIComponent(userLaunchUrl.href)}`, origin);
}

if (isCDSHooksLaunch) {
userLaunchUrl = new URL("/launch.html", ENV.CDS_SANDBOX_URL)
userLaunchUrl.searchParams.set("launch", launchCode);
userLaunchUrl.searchParams.set("iss", iss);
}

let validationErrors = getValidationErrors(launch, query);

return (
<HelmetProvider>
<Helmet>
Expand All @@ -190,9 +205,9 @@ export default function Launcher() {
<li role="presentation" className={ tab === "0" ? "active" : undefined } onClick={ () => setQuery({ tab: "0" }) }>
<b role="tab">App Launch Options</b>
</li>
<li role="presentation" className={ tab === "1" ? "active" : undefined } onClick={ () => setQuery({ tab: "1" }) }>
{ !isCDSHooksLaunch && <li role="presentation" className={ tab === "1" ? "active" : undefined } onClick={ () => setQuery({ tab: "1" }) }>
<b role="tab">Client Registration & Validation</b>
</li>
</li> }
</ul>
<form onSubmit={e => e.preventDefault()}>
<div className="tab-content">
Expand All @@ -208,7 +223,11 @@ export default function Launcher() {
<div className="mt-2" style={{ background: "#F3F3F3", padding: "10px 15px", borderRadius: 5 }}>
<h4 className="text-primary mt-0">
<i className="glyphicon glyphicon-fire"/> {
isStandaloneLaunch || launch_type === "backend-service" ? "Server's FHIR Base URL" : "App's Launch URL"
isStandaloneLaunch || launch_type === "backend-service" ?
"Server's FHIR Base URL" :
launch_type === "cds-hooks" ?
"Discovery Endpoint URL" :
"App's Launch URL"
}
</h4>
<div style={{ display: "flex" }}>
Expand All @@ -221,9 +240,15 @@ export default function Launcher() {
value={ isStandaloneLaunch || isBackendService ? aud : launch_url }
onChange={ e => !isStandaloneLaunch && !isBackendService && setQuery({ launch_url: e.target.value }) }
readOnly={ isStandaloneLaunch || isBackendService }
placeholder={ isStandaloneLaunch || isBackendService ?
undefined :
launch_type === "cds-hooks" ?
"Discovery Endpoint URL" :
"Launch URL"
}
/>
<span className="input-group-btn">
{ isStandaloneLaunch || isBackendService ?
{ (isStandaloneLaunch || isBackendService) ?
<button className="btn btn-primary" onClick={() => copyElement("#launch-url")}>Copy</button> :
validationErrors.length ?
<button className="btn btn-default" disabled>Launch</button> :
Expand All @@ -238,19 +263,24 @@ export default function Launcher() {
</span>
</div>
</div>
<div style={{ flex: "1 1 0", marginLeft: 5 }}>
{ launch_type !== "cds-hooks" && <div style={{ flex: "1 1 0", marginLeft: 5 }}>
{ validationErrors.filter(e => e !== "Missing app launch URL" && e !== "Invalid app launch URL").length ?
<button className="btn btn-default" disabled>Launch Sample App</button> :
<a href={sampleLaunchUrl.href} target="_blank" rel="noreferrer noopener" className="btn btn-default">
<span className="text-success">Launch Sample App</span>
</a>
}
</div>
</div> }
</div>
{ isStandaloneLaunch || launch_type === "backend-service" ?
{ (isStandaloneLaunch || launch_type === "backend-service") ?
<span className="small text-muted">
Your app should use this url to connect to the sandbox FHIR server
</span> :
launch_type === "cds-hooks" ?
<span className="small text-muted">
If you have developed CDS service(s) enter your discovery endpoint
URL and click "Launch" to launch the CDS Hooks Sandbox.
</span> :
<span className="small text-muted">
Full url of the page in your app that will initialize the
SMART session (often the path to a launch.html file or endpoint)
Expand Down Expand Up @@ -317,7 +347,7 @@ function LaunchTab() {
</div>

<div className="row">
<div className="col-md-6">
<div className={ launch.launch_type === "cds-hooks" ? "col-md-12" : "col-md-6" }>
<div className="form-group">
<label htmlFor="fhir_version" className="text-primary">FHIR Version</label>
<select
Expand All @@ -335,7 +365,7 @@ function LaunchTab() {
</span>
</div>
</div>
<div className="col-md-6">
{ launch.launch_type !== "cds-hooks" && <div className="col-md-6">
<div className="form-group">
<label htmlFor="sim_error" className="text-primary">Simulated Error</label>
<select
Expand Down Expand Up @@ -370,10 +400,10 @@ function LaunchTab() {
Force the server to throw certain type of error (useful for manual testing).
</span>
</div>
</div>
</div> }
</div>

{ launch.launch_type !== "backend-service" &&
{ launch.launch_type !== "backend-service" && launch.launch_type !== "cds-hooks" &&
<div className="form-group">
<div style={{ borderBottom: "1px solid #EEE" }}>
<label className="text-primary">Misc. Options</label>
Expand Down Expand Up @@ -444,10 +474,9 @@ function LaunchTab() {
}}
/>
<span className="help-block small">
Simulates the active patient in EHR when app is launched. If
no Patient ID is entered or if multiple comma delimited IDs
are specified, a patient picker will be displayed as part of
the launch flow.
Simulates the active patient in EHR when { launch.launch_type === "cds-hooks" ? "the CDS sandbox" : "app" } is
launched. If no Patient ID is entered or if multiple comma delimited IDs are specified, a patient picker will
be displayed as part of the launch flow.
</span>
</div>

Expand All @@ -466,10 +495,9 @@ function LaunchTab() {
}}
/>
<span className="help-block small">
Simulates user who is launching the app. If no provider is
selected, or if multiple comma delimited Practitioner IDs
are specified, a login screen will be displayed as part of
the launch flow.
Simulates user who is launching the { launch.launch_type === "cds-hooks" ? "CDS sandbox" : "app" }.
If no provider is selected, or if multiple comma delimited Practitioner IDs are specified,
a login screen will be displayed as part of the launch flow.
</span>
</div>
)}
Expand Down
3 changes: 2 additions & 1 deletion src/isomorphic/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export const launchTypes: SMART.LaunchType[] = [
"patient-portal",
"provider-standalone",
"patient-standalone",
"backend-service"
"backend-service",
"cds-hooks"
];

export const clientTypes: SMART.SMARTClientType[] = [
Expand Down

0 comments on commit 703910c

Please sign in to comment.