chore(version): update cs3d version and add iframe documentation (#3474)

This commit is contained in:
Alireza authored and GitHub committed 2023-06-20 10:26:05 -04:00
1 parent 5670a1699d
commit 83c453d1f3
428 files changed
+271 -9666

No files matched your search

@@ -0,0 +1,4 @@
{
"label": "Deployment",
"position": 8
}
@@ -0,0 +1,309 @@
---
sidebar_position: 1
sidebar_label: Overview
---
# Deployment
The OHIF Viewer can be embedded in other web applications via it's [packaged
script source][viewer-npm], or served up as a stand-alone PWA ([progressive web
application][pwa-url]) by building and hosting a collection of static assets. In
either case, you will need to configure your instance of the Viewer so that it
can connect to your data source (the database or PACS that provides the data
your Viewer will display).
Our goal is to make deployment as simple and painless as possible; however,
there is an inherent amount of complexity in configuring and deploying web
applications. If you find yourself a little lost, please don't hesitate to
[reach out for help](/help.md)
## Deployment Scenarios
### Embedded Viewer
The quickest and easiest way to get the OHIF Viewer up and running is to embed
it into an existing web application. It allows us to forego a "build step", and
add a powerful medical imaging viewer to an existing web page using only a few
include tags.
- Read more about it here: [Embedded Viewer](./recipes/embedded-viewer.md)
- And check out our [live demo on CodeSandbox][code-sandbox]
![embeddedViewer](../assets/img/embedded-viewer-diagram.png)
### Stand-alone Viewer
Deploying the OHIF Viewer as a stand-alone web application provides many
benefits, but comes at the cost of time and complexity. Some benefits include:
_Today:_
- Leverage [extensions](/extensions/index.md) to drop-in powerful new features
- Add routes and customize the viewer's workflow
- Finer control over styling and whitelabeling
_In the future:_
- The ability to package the viewer for [App Store distribution][app-store]
- Leverage `service-workers` for offline support and speed benefits from caching
#### Hosted Static Assets
At the end of the day, a production OHIF Viewer instance is a collection of
HTML, CSS, JS, Font Files, and Images. We "build" those files from our
`source code` with configuration specific to our project. We then make those
files publicly accessible by hosting them on a Web Server.
If you have not deployed a web application before, this may be a good time to
[reach out for help](/help.md), as these steps assume prior web development and
deployment experience.
##### Part 1 - Build Production Assets
"Building", or creating, the files you will need is the same regardless of the
web host you choose. You can find detailed instructions on how to configure and
build the OHIF Viewer in our
["Build for Production" guide](./recipes/build-for-production.md).
##### Part 2 - Host Your App
There are a lot of [benefits to hosting static assets][host-static-assets] over
dynamic content. You can find instructions on how to host your build's output
via one of these guides:
_Drag-n-drop_
- [Netlify: Drop](/deployment/recipes/static-assets.md#netlify-drop)
_Easy_
- [Surge.sh](/deployment/recipes/static-assets.md#surgesh)
- [GitHub Pages](/deployment/recipes/static-assets.md#github-pages)
_Advanced_
- [AWS S3 + Cloudfront](/deployment/recipes/static-assets.md#aws-s3--cloudfront)
- [GCP + Cloudflare](/deployment/recipes/static-assets.md#gcp--cloudflare)
- [Azure](/deployment/recipes/static-assets.md#azure)
## Data
The OHIF Viewer is able to connect to any data source that implements the [DICOM
Web Standard][dicom-web-standard]. [DICOM Web][dicom-web] refers to RESTful
DICOM Services -- a recently standardized set of guidelines for exchanging
medical images and imaging metadata over the internet. Not all archives fully
support it yet, but it is gaining wider adoption.
### Configure Connection
If you have an existing archive and intend to host the OHIF Viewer at the same
domain name as your archive, then connecting the two is as simple as following
the steps layed out in our
[Configuration Essentials Guide](./../configuring/index.md).
#### What if I don't have an imaging archive?
We provide some guidance on configuring a local image archive in our
[Data Source Essentials](./../configuring/data-source.md) guide. Hosting an
archive remotely is a little trickier. You can check out some of our
[advanced recipes](#recipes) for modeled setups that may work for you.
#### What if I intend to host the OHIF Viewer at a different domain?
There are two important steps to making sure this setup works:
1. Your Image Archive needs to be exposed, in some way, to the open web. This
can be directly, or through a `reverse proxy`, but the Viewer needs _some
way_ to request it's data.
2. \* Your Image Archive needs to have appropriate CORS (Cross-Origin Resource
Sharing) Headers
> \* Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional
> HTTP headers to tell a browser to let a web application running at one origin
> (domain) have permission to access selected resources from a server at a
> different origin. - [MDN Web Docs: Web - Http - CORS][cors]
Most image archives do not provide either of these features "out of the box".
It's common to use IIS, Nginx, or Apache to route incoming requests and append
appropriate headers. You can find an example of this setup in our
[Nginx + Image Archive Deployment Recipe](./recipes/nginx--image-archive.md).
#### What if my archive doesn't support DicomWeb?
It's possible to supply all Study data via JSON format, in the event you do not have a DicomWeb endpoint.
You can host all of the relevant files on any web accessible server (Amazon S3, Azure Blob Storage, Local file server etc.)
This JSON is supplied via the '?url=' query parameter.
It should reference an endpoint that returns **application/json** formatted text.
If you do not have an API, you can simply return a text file containing the JSON from any web server.
You tell the OHIF viewer to use JSON by appending the `'?url='` query to the `/Viewer` route:
eg. `https://my-test-ohif-server/viewer?url=https://my-json-server/study-uid.json`
The returned JSON object must contain a single root object with a 'studies' array.
*Sample JSON format:*
```json
{
"studies": [
{
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78",
"StudyDescription": "BRAIN SELLA",
"StudyDate": "20010108",
"StudyTime": "120022",
"PatientName": "MISTER^MR",
"PatientId": "832040",
"series": [
{
"SeriesDescription": "SAG T-1",
"SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
"SeriesNumber": 2,
"SeriesDate": "20010108",
"SeriesTime": "120318",
"Modality": "MR",
"instances": [
{
"metadata": {
"Columns": 512,
"Rows": 512,
"InstanceNumber": 3,
"AcquisitionNumber": 0,
"PhotometricInterpretation": "MONOCHROME2",
"BitsAllocated": 16,
"BitsStored": 16,
"PixelRepresentation": 1,
"SamplesPerPixel": 1,
"PixelSpacing": [0.390625, 0.390625],
"HighBit": 15,
"ImageOrientationPatient": [0,1,0,0,0,-1],
"ImagePositionPatient": [11.600000,-92.500000, 98.099998],
"FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470",
"ImageType": ["ORIGINAL","PRIMARY","OTHER"],
"Modality": "MR",
"SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.124",
"SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78"
},
"url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.124.dcm"
}
]
}
]
}
]
}
```
More info on this JSON format can be found here [Issue #1500](https://github.com/OHIF/Viewers/issues/1500)
**Implementation Notes:**
1. When hosting the viewer, you will also need to host a /viewer route on the server - or the browser may not be able to find the route.
2. For each instance url (dicom object) in the returned JSON, you must prefix the `url` with `dicomweb:` in order for the cornerstone image loader to retrieve it correctly.
eg. `https://image-server/my-image.dcm` ---> `dicomweb:https://image-server/my-image.dcm`
3. The JSON format above is compatible with >= v3.7.8 of the application. Older versions of the viewer used a different JSON format. As of 20/04/20 the public [https://viewer.ohif.org/] is a pre 3.0 version that does not support this format yet.
4. The JSON format is case-sensitive. Please ensure you have matched casing with the naturalised Dicom format referenced in [Issue #1500](https://github.com/OHIF/Viewers/issues/1500).
*CORS Issues (Cross-Origin Resource Sharing)*
If you host a JSON API or Images on a different domain from the the app itself, you will likely have CORS issues. This will also happen when testing from Localhost and reaching out to remote servers.
Even if the domain is the same, different ports, subdomains or protocols (https vs http) will also cause CORS errors.
You will to need add a configuration on each server hosting these assets to allow your App server origin.
For example:
Lets assume your application is hosted on `https://my-ohif-server.com`.
Your JSON API is hosted on `https://my-json-api.aws.com`
And your images are stored on Amazon S3 at `https://my-s3-bucket.aws.com`
When you first start your application, browsing to `https://my-ohif-server.com/viewer?url=https://my-json-api.aws.com/api/my-json-study-info.json`, you will likely get a CORS error in the browser console as it tries to connect to `https://my-json-api.aws.com`.
Adding a setting on the JSON server to allow the CORS origin = `https://my-ohif-server.com` should solve this.
Next, you will likely get a similar CORS error, as the browser tries to go to `https://my-s3-bucket.aws.com`.
You will need to go to the S3 bucket configuration, and add a CORS setting to allow origin = `https://my-ohif-server.com`.
Essentially, whenever the application connects to a remote resource, you will need to add the applications url to the allowed CORS Origins on that resource. Adding an origin similar to https://localhost:3000 will also allow for local testing.
### Securing Your Data
> Feeling lost? Securing your data is important, and it can be hard to tell if
> you've gotten it right. Don't hesitate to work with professional auditors, or
> [enlist help from experts](./../help.md).
The OHIF Viewer can be configured to work with authorization servers that
support one or more of the OpenID-Connect authorization flows. The Viewer finds
it's OpenID-Connect settings on the `oidc` configuration key. You can set these
values following the instructions laid out in the
[Configuration Essentials Guide](./../configuring/index.md).
_Example OpenID-Connect Settings:_
```js
window.config = {
...
oidc: [
{
// ~ REQUIRED
// Authorization Server URL
authority: 'http://127.0.0.1/auth/realms/ohif',
client_id: 'ohif-viewer',
redirect_uri: 'http://127.0.0.1/callback', // `OHIFStandaloneViewer.js`
response_type: 'code', // "Authorization Code Flow"
scope: 'openid', // email profile openid
// ~ OPTIONAL
post_logout_redirect_uri: '/logout-redirect.html',
},
],
}
```
You can find an example of this setup in our
[User Account Control Deployment Recipe](./recipes/user-account-control.md).
#### Choosing a Flow for the Viewer
In general, we recommend using the "Authorization Code Flow" ( [see
`response_type=code` here][code-flows]); however, the "Implicit Flow" ( [see
`response_type=token` here][code-flows]) can work if additonal precautions are
taken. If the flow you've chosen produces a JWT Token, it's validity can be used
to secure access to your Image Archive as well.
### Recipes
We've included a few recipes for common deployment scenarios. There are many,
many possible configurations, so please don't feel limited to these setups.
Please feel free to suggest or contribute your own recipes.
- Script Include
- [Embedding the Viewer](./recipes/embedded-viewer.md)
- Stand-Alone
- [Build for Production](./recipes/build-for-production.md)
- [Static](./recipes/static-assets.md)
- [Nginx + Image Archive](./recipes/nginx--image-archive.md)
- [User Account Control](./recipes/user-account-control.md)
<!--
Links
-->
<!-- prettier-ignore-start -->
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
[pwa-url]: https://developers.google.com/web/progressive-web-apps/
[static-assets-url]: https://www.maxcdn.com/one/visual-glossary/static-content/
[app-store]: https://medium.freecodecamp.org/i-built-a-pwa-and-published-it-in-3-app-stores-heres-what-i-learned-7cb3f56daf9b
[dicom-web-standard]: https://www.dicomstandard.org/dicomweb/
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
[host-static-assets]: https://www.netlify.com/blog/2016/05/18/9-reasons-your-site-should-be-static/
[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
[code-flows]: https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660
[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
<!-- prettier-ignore-end -->
@@ -0,0 +1,4 @@
{
"label": "Recipes",
"position": 2
}
@@ -0,0 +1,138 @@
---
sidebar_position: 2
---
# Build for Production
> If you've already followed the
> ["Getting Started" Guide](/development/getting-started.md), you can skip ahead
> to [Configuration](#configuration)
## Overview
### Build Machine Requirements
- [Node.js & NPM](https://nodejs.org/en/download/)
- [Yarn](https://yarnpkg.com/lang/en/docs/install/)
- [Git](https://www.atlassian.com/git/tutorials/install-git)
### Getting the Code
_With Git:_
```bash
# Clone the remote repository to your local machine
git clone https://github.com/OHIF/Viewers.git
```
More on: _[`git clone`](https://git-scm.com/docs/git-clone),
[`git checkout`](https://git-scm.com/docs/git-checkout)_
_From .zip:_
[OHIF/Viewers: react.zip](https://github.com/OHIF/Viewers/archive/master.zip)
### Restore Dependencies & Build
Open your terminal, and navigate to the directory containing the source files.
Next run these commands:
```bash
// If you haven't already, enable yarn workspaces
yarn config set workspaces-experimental true
// Restore dependencies
yarn install
// Build source code for production
yarn run build
```
If everything worked as expected, you should have a new `dist/` directory in the
project's folder. It should roughly resemble the following:
```bash
<root>platform/viewer/dist/
├── app-config.js
├── app.bundle.js
├── app.css
├── index.html
├── manifest.json
├── service-worker.js
└── ...
```
By default, the build output will connect to OHIF's publicly accessible PACS. If
this is your first time setting up the OHIF Viewer, it is recommended that you
test with these default settings. After testing, you can find instructions on
how to configure the project for your own imaging archive below.
### Configuration
The configuration for our viewer is in the `<root>platform/viewer/public/config`
directory. Our build process knows which configuration file to use based on the
`APP_CONFIG` environment variable. By default, its value is
[`config/default.js`][default-config]. The majority of the viewer's features,
and registered extension's features, are configured using this file.
The easiest way to apply your own configuration is to modify the `default.js`
file. For more advanced cofiguration options, check out our
[configuration essentials guide](/configuring/index.md).
## Next Steps
### Deploying Build Output
_Drag-n-drop_
- [Netlify: Drop](/deployment/recipes/static-assets.md#netlify-drop)
_Easy_
- [Surge.sh](/deployment/recipes/static-assets.md#surgesh)
- [GitHub Pages](/deployment/recipes/static-assets.md#github-pages)
_Advanced_
- [AWS S3 + Cloudfront](/deployment/recipes/static-assets.md#aws-s3--cloudfront)
- [GCP + Cloudflare](/deployment/recipes/static-assets.md#gcp--cloudflare)
- [Azure](/deployment/recipes/static-assets.md#azure)
### Testing Build Output Locally
A quick way to test your build output locally is to spin up a small webserver.
You can do this by running the following commands in the `dist/` output
directory:
```bash
// Install http-server as a globally available package
yarn global add http-server
// Serve the files in our current directory
// Accessible at: `http://localhost:8080`
http-server
```
### Automating Builds and Deployments
If you found setting up your environmnent and running all of these steps to be a
bit tedious, then you are in good company. Thankfully, there are a large number
of tools available to assist with automating tasks like building and deploying
web application. For a starting point, check out this repository's own use of:
- [CircleCI][circleci]: [config.yaml][circleci-config]
- [Netlify][netlify]: [netlify.toml][netlify.toml] |
[build-deploy-preview.sh][build-deploy-preview.sh]
## Troubleshooting
> Issues and resolutions for common GitHub issues will be summarized here
...
<!-- prettier-ignore-start -->
[circleci]: https://circleci.com/gh/OHIF/Viewers
[circleci-config]: https://github.com/OHIF/Viewers/blob/master/.circleci/config.yml
[netlify]: https://app.netlify.com/sites/ohif/deploys
[netlify.toml]: https://github.com/OHIF/Viewers/blob/master/netlify.toml
[build-deploy-preview.sh]: https://github.com/OHIF/Viewers/blob/master/.netlify/build-deploy-preview.sh
<!-- prettier-ignore-end -->
@@ -0,0 +1,179 @@
---
sidebar_position: 1
---
# Embedded Viewer
The quickest and easiest way to get the OHIF Viewer up and running is to embed
it into an existing web application. It allows us to forego a "build step", and
add a powerful medical imaging viewer to an existing web page using only a few
include tags. Here's how it works:
![embeddedViewer](../../assets/img/embedded-viewer-diagram.png)
1. Create a new web page or template that includes the following external
dependencies:
<ul>
<li>
<a href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap">
<code>Google Font: Roboto</code>
</a>
</li>
<li>
<a href="https://unpkg.com/@ohif/viewer">
<code>@ohif/viewer@latest</code>
</a>
</li>
</ul>
<ol start="2">
<li>Create a JS Object or Function to hold the OHIF Viewer's configuration. Here are some
example values that would allow the viewer to hit our public PACS:</li>
</ol>
```js
// Set before importing `ohif-viewer` (JS Object)
window.config = {
// default: '/'
routerBasename: '/',
servers: {
dicomWeb: [
{
name: 'DCM4CHEE',
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
},
],
},
};
```
To learn more about how you can configure the OHIF Viewer, check out our
[Configuration Guide](../../configuring/index.md).
<ol start="3"><li>
Render the viewer in the web page's target <code>div</code>
</li></ol>
```js
// Made available by the `@ohif/viewer` script included in step 1
var containerId = 'id-of-div-to-render-component-to';
var componentRenderedOrUpdatedCallback = function() {
console.log('OHIF Viewer rendered/updated');
};
window.OHIFViewer.installViewer(
window.config,
containerId,
componentRenderedOrUpdatedCallback
);
```
You can see a live example of this recipe in [this CodeSandbox][code-sandbox].
## Add Extensions
The UMD build of the OHIF Viewer is a "light weight" build that only contains
the core extensions required for basic 2D image viewing. It's possible to add
other extensions at runtime.
This only requires us to include a single script tag, and add it using the
`extensions` key to our config. In this practical example, we register our
popular whole slide microscopy extension:
```html
<script
src="https://unpkg.com/@ohif/extension-dicom-microscopy@0.50.5/dist/index.umd.js"
crossorigin
></script>
<!-- --->
<script>
window.config = {
// ...
extensions: [OHIFExtDicomMicroscopy],
};
</script>
```
You can see an example of a slide microscopy study in the viewer [with the
extension enabled here][whole-slide-ext-demo] ([source code][ext-code-sandbox])
and [without it here][whole-slide-base-demo] ([source code][code-sandbox]).
You can read more about extensions and how to create your own in our
[extensions guide](/extensions/index.md).
#### FAQ
> I'm having trouble getting this to work. Where can I go for help?
First, check out this fully functional [CodeSandbox][code-sandbox] example. If
you're still having trouble, feel free to search or GitHub issues. Can't find
anything related your problem? Create a new one.
> My application's styles are impacting the OHIF Viewer's look and feel. What
> can I do?
When you include stylesheets and scripts, they are added globally. This has the
potential of causing conflicts with other scripts and styles on the page. To
prevent this, `embed` the viewer in a new/empty web page. Have that working?
Good. Now `embed` that new page using an
[`<iframe>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe).
This should produce the expected result while also protecting your page from any
globally defined styles/scripts.
> We're trying to embed the OHIF Viewer into an existing React App, but seeing
> react-dom and react conflicts. What can we do?
If you are installing OHIF viewer inside another react app, you may use `installViewer` as follows:
```jsx
import { installViewer } from '@ohif/viewer'
const ohifViewerConfig = window.config // or set it here
const containerId = 'ohif'
const componentRenderedOrUpdatedCallback = function() {
console.log('OHIF Viewer rendered/updated');
};
componentDidMount() {
installViewer(
ohifViewerConfig,
containerId,
componentRenderedOrUpdatedCallback
);
}
render () {
...
//you can render in any element you wish
<AnyTag id={containerId}/>
}
```
`installViewer` is a convenience method that pulls in some dependencies that may
not be compatible with existing `react` apps. `@ohif/viewer` also exports `App`
which is a react component that takes the `configuration` outlined above as
props. You can use it as a reusable component, and to avoid `react` version
conflict issues.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
[whole-slide-base-demo]: https://tprch.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2020.1519719354.757
[ext-code-sandbox]: https://codesandbox.io/s/viewer-script-tag-microscopy-extension-44unk
[whole-slide-ext-demo]: https://44unk.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2448.1519719572.518
<!-- prettier-ignore-end -->
@@ -0,0 +1,75 @@
---
sidebar_position: 6
---
# Google Cloud Healthcare
> The [Google Cloud Healthcare API](https://cloud.google.com/healthcare/) is a
> powerful option for storing medical imaging data in the cloud.
An alternative to deploying your own PACS is to use a software-as-a-service
provider such as Google Cloud. The Cloud Healthcare API promises to be a
scalable, secure, cost effective image storage solution for those willing to
store their data in the cloud. It offers an
[almost-entirely complete DICOMWeb API](https://cloud.google.com/healthcare/docs/dicom)
which requires tokens generated via the
[OAuth 2.0 Sign In flow](https://developers.google.com/identity/sign-in/web/sign-in).
Images can even be transcoded on the fly if this is desired.
## Setup a Google Cloud Healthcare Project
- Create a Google Cloud account
- Create a project in Google Cloud
- Enable the [Cloud Healthcare API](https://cloud.google.com/healthcare/) for
your project.
- (Optional): Create a Dataset and Data Store for storing your DICOM data
- Enable the
[Cloud Resource Manager API](https://cloud.google.com/resource-manager/) for
your project.
- _Note:_ If you are having trouble finding the APIs, use the search box at
the top of the Cloud console.
- Go to APIs & Services > Credentials to create an OAuth Consent screen and fill
in your application details.
- Under Scopes for Google APIs, click "manually paste scopes".
- Add the following scopes:
- `https://www.googleapis.com/auth/cloudplatformprojects.readonly`
- `https://www.googleapis.com/auth/cloud-healthcare`
- Go to APIs & Services > Credentials to create a new set of credentials:
- Choose the "Web Application" type
- Set up an
[OAuth 2.0 Client ID](https://support.google.com/cloud/answer/6158849?hl=en)
- Add your domain (e.g. `http://localhost:3000`) to Authorized JavaScript
origins.
- Add your domain, plus `callback` (e.g. `http://localhost:3000/callback`) to
Authorized Redirect URIs.
- Save your Client ID for later.
- (Optional): Enable Public Datasets that are being hosted by Google:
https://cloud.google.com/healthcare/docs/resources/public-datasets/
## Run the viewer with your OAuth Client ID
1. Open the `config/google.js` file and change `YOURCLIENTID` to your Client ID
value.
1. Run the OHIF Viewer using the config/google.js configuration file
```bash
cd OHIFViewer
yarn install
APP_CONFIG=config/google.js yarn run dev
```
## Running via Docker
The OHIF Viewer Docker container can be connected to Google Cloud Healthcare by
providing a Client ID at runtime. This is a very simple method to get up and
running.
1. Install Docker (https://www.docker.com/)
1. Run the Docker container, providing a Client ID as an environment variable.
Client IDs look like `xyz.apps.googleusercontent.com`.
```bash
docker run --env CLIENT_ID=$CLIENT_ID --publish 5000:80 ohif/viewer:latest
```
@@ -0,0 +1,246 @@
---
sidebar_position: 4
---
# Nginx + Image Archive
> DISCLAIMER! We make no claims or guarantees of this approach's security. If in
> doubt, enlist the help of an expert and conduct proper audits.
At a certain point, you may want others to have access to your instance of the
OHIF Viewer and its medical imaging data. This post covers one of many potential
setups that accomplish that. Please note, noticably absent is user account
control.
Do not use this recipe to host sensitive medical data on the open web. Depending
on your company's policies, this may be an appropriate setup on an internal
network when protected with a server's basic authentication. For a more robust
setup, check out our [user account control recipe](./user-account-control.md)
that builds on the lessons learned here.
## Overview
Our two biggest hurdles when hosting our image archive and web client are:
- Risks related to exposing our PACS to the netowrk
- Cross-Origin Resource Sharing (CORS) requests
### Handling Web Requests
We mittigate our first issue by allowing [Nginx][nginx] to handle incoming web
requests. Nginx is open source software for web serving, reverse proxying,
caching, and more. It's designed for maximum performance and stability --
allowing us to more reliably serve content than Orthanc's built-in server can.
More specifically, we accomplish this by using a
[`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) to retrieve
resources from our image archive (Orthanc), and when accessing its web admin.
> A reverse proxy is a type of proxy server that retrieves resources on behalf
> of a client from one or more servers. These resources are then returned to the
> client, appearing as if they originated from the proxy server itself.
### CORS Issues
Cross-Origin Resource Sharing (CORS) is a mechanism that uses HTTP headers to
tell a browser which web applications have permission to access selected
resources from a server at a different origin (domain, protocol, port). IE. By
default, a Web App located at `http://my-website.com` can't access resources
hosted at `http://not-my-website.com`
We can solve this one of two ways:
1. Have our Image Archive located at the same domain as our Web App
2. Add appropriate `Access-Control-Allow-*` HTTP headers
**This solution uses the first approach.**
You can read more about CORS in this Medium article: [Understanding
CORS][understanding-cors]
### Diagram
This setup allows us to create a setup similar to the one pictured below:
![nginX](../../assets/img/nginx-image-archive.png)
- All web requests are routed through `nginx` on our `OpenResty` image
- `/pacs` is a reverse proxy for `orthanc`'s `DICOM Web` endpoints
- `/pacs-admin` is a reverse proxy for `orthanc`'s Web Admin
- All static resources for OHIF Viewer are served up by `nginx` when a matching
route for that resource is requested
## Getting Started
### Requirements
- Docker
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
- [Docker for Windows](https://docs.docker.com/docker-for-windows/)
_Not sure if you have `docker` installed already? Try running `docker --version`
in command prompt or terminal_
### Setup
_Spin Things Up_
- Navigate to `<project-root>/docker/OpenResty-Orthanc` in your shell
- Run `docker-compose up`
_Upload Your First Study_
- Navigate to `http://127.0.0.1/pacs-admin`
- From the top right, select "Upload"
- Click "Select files to upload..." (DICOM)
- Click "Start the upload"
- Navigate back to `http://127.0.0.1/` to view your studies in the Study List
### Troubleshooting
_Exit code 137_
This means Docker ran out of memory. Open Docker Desktop, go to the `advanced`
tab, and increase the amount of Memory available.
_Cannot create container for service X_
Use this one with caution: `docker system prune`
_X is already running_
Stop running all containers:
- Win: `docker ps -a -q | ForEach { docker stop $_ }`
- Linux: `docker stop $(docker ps -a -q)`
### Configuration
After verifying that everything runs with default configuration values, you will
likely want to update:
- The domain: `http://127.0.0.1`
#### OHIF Viewer
The OHIF Viewer's configuration is imported from a static `.js` file. The
configuration we use is set to a specific file when we build the viewer, and
determined by the env variable: `APP_CONFIG`. You can see where we set its value
in the `dockerfile` for this solution:
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
You can find the configuration we're using here:
`/public/config/docker_openresty-orthanc.js`
To rebuild the `webapp` image created by our `dockerfile` after updating the
Viewer's configuration, you can run:
- `docker-compose build` OR
- `docker-compose up --build`
#### Other
All other files are found in: `/docker/OpenResty-Orthanc/`
| Service | Configuration | Docs |
| ----------------- | --------------------------------- | ------------------------------------------- |
| OHIF Viewer | [dockerfile][dockerfile] | You're reading them now! |
| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] |
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
## Next Steps
### Deploying to Production
While these configuration and docker-compose files model an environment suitable
for production, they are not easy to deploy "as is". You can either:
- Manually recreate this environment and deploy built application files **OR**
- Deploy to a cloud kubernetes provider like
[Digital Ocean](https://www.digitalocean.com/products/kubernetes/) **OR**
- [See a full list of cloud providers here](https://landscape.cncf.io/category=cloud&format=card-mode&grouping=category)
- Find and follow your preferred provider's guide on setting up
[swarms and stacks](https://docs.docker.com/get-started/)
### Adding SSL
Adding SSL registration and renewal for your domain with Let's Encrypt that
terminates at Nginx is an incredibly important step toward securing your data.
Here are some resources, specific to this setup, that may be helpful:
- [lua-resty-auto-ssl](https://github.com/GUI/lua-resty-auto-ssl)
- [Let's Encrypt + Nginx](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/)
While we terminate SSL at Nginx, it may be worth using self signed certificates
for communication between services.
- [SSL Termination for TCP Upstream Servers](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-tcp/)
### Use PostgresSQL w/ Orthanc
Orthanc can handle a large amount of data and requests, but if you find that
requests start to slow as you add more and more studies, you may want to
configure your Orthanc instance to use PostgresSQL. Instructions on how to do
that can be found in the
[`Orthanc Server Book`](http://book.orthanc-server.com/users/docker.html), under
"PostgreSQL and Orthanc inside Docker"
### Improving This Guide
Here are some improvements this guide would benefit from, and that we would be
more than happy to accept Pull Requests for:
- SSL Support
- Complete configuration with `.env` file (or something similar)
- Any security issues
- One-click deploy to a cloud provider
## Resources
### Misc. Helpful Commands
_Check if `nginx.conf` is valid:_
```bash
docker run --rm -t -a stdout --name my-openresty -v $PWD/config/:/usr/local/openresty/nginx/conf/:ro openresty/openresty:alpine-fat openresty -c /usr/local/openresty/nginx/conf/nginx.conf -t
```
_Interact w/ running container:_
`docker exec -it CONTAINER_NAME bash`
_List running containers:_
`docker ps`
### Referenced Articles
For more documentation on the software we've chosen to use, you may find the
following resources helpful:
- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html)
- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/)
- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/)
For a different take on this setup, check out the repositories our community
members put together:
- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker)
- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker)
<!--
Links
-->
<!-- prettier-ignore-start -->
<!-- DOCS -->
[nginx]: https://www.nginx.com/resources/glossary/nginx/
[understanding-cors]: https://medium.com/@baphemot/understanding-cors-18ad6b478e2b
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
<!-- SRC -->
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/nginx.conf
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/orthanc.json
<!-- prettier-ignore-end -->
@@ -0,0 +1,160 @@
---
sidebar_position: 3
sidebar_label: Assets
---
# Deploy Static Assets
> WARNING! All of these solutions stand-up a publicly accessible web viewer. Do
> not hook your hosted viewer up to a sensitive source of data without
> implementing authentication.
There are a lot of options for deploying static assets. Some services, like
`netlify` and `surge.sh`, specialize in static websites. You'll notice that
deploying with them requires much less time and effort, but comes at the cost of
less product offerings.
While not required, it can simplify things to host your Web Viewer alongside
your image archive. Services with more robust product offerings, like
`Google Cloud`, `Microsoft's Azure`, and `Amazon Web Services (AWS)`, are able
to accomodate this setup.
_Drag-n-drop_
- [Netlify: Drop](#netlify-drop)
_Easy_
- [Surge.sh](#surgesh)
- [GitHub Pages](#github-pages)
_Advanced_
- [Deploy Static Assets](#deploy-static-assets)
- [Drag-n-drop](#drag-n-drop)
- [Netlify: Drop](#netlify-drop)
- [Easy](#easy)
- [Surge.sh](#surgesh)
- [GitHub Pages](#github-pages)
- [Advanced](#advanced)
- [AWS S3 + Cloudfront](#aws-s3--cloudfront)
- [GCP + Cloudflare](#gcp--cloudflare)
- [Azure](#azure)
## Drag-n-drop
### Netlify: Drop
> Build, deploy, and manage modern web projects. An all-in-one workflow that
> combines global deployment, continuous integration, and automatic HTTPS. And
> that’s just the beginning.
![netlify-drop example](../../assets/img/netlify-drop.gif)
_GIF demonstrating deployment with Netlify Drop_
1. https://app.netlify.com/drop
2. Drag your `build/` folder on to the drop target
3. ...
4. _annnd you're done_
**Features:**
- Custom domains & HTTPS
- Instant Git integration
- Continuous deployment
- Deploy previews
- Access to add-ons
(Non-free tiers include identity, FaaS, Forms, etc.)
Learn more about [Netlify on their website](https://www.netlify.com/)
## Easy
### Surge.sh
> Static web publishing for Front-End Developers. Simple, single-command web
> publishing. Publish HTML, CSS, and JS for free, without leaving the command
> line.
![surge.sh deploy example](../../assets/img/surge-deploy.gif)
_GIF demonstrating deployment with surge_
```shell
# Add surge command
yarn global add surge
# In the build directory
surge
```
**Features:**
- Free custom domain support
- Free SSL for surge.sh subdomains
- pushState support for single page apps
- Custom 404.html pages
- Barrier-free deployment through the CLI
- Easy integration into your Grunt toolchain
- Cross-origin resource support
- And more…
Learn more about [surge.sh on their website](https://surge.sh/)
### GitHub Pages
> WARNING! While great for project sites and light use, it is not advised to use
> GitHub Pages for production workloads. Please consider using a different
> service for mission critical applications.
> Websites for you and your projects. Hosted directly from your GitHub
> repository. Just edit, push, and your changes are live.
This deploy strategy makes more sense if you intend to maintain your project in
a GitHub repository. It allows you to specify a `branch` or `folder` as the
target for a GitHub Page's website. As you push code changes, the hosted content
updates to reflect those changes.
1. Head over to GitHub.com and create a new repository, or go to an existing
one. Click on the Settings tab.
2. Scroll down to the GitHub Pages section. Choose the `branch` or `folder` you
would like as the "root" of your website.
3. Fire up a browser and go to `http://username.github.io/repository`
Configuring Your Site:
- [Setting up a custom domain](https://help.github.com/en/articles/using-a-custom-domain-with-github-pages)
- [Setting up SSL](https://help.github.com/en/articles/securing-your-github-pages-site-with-https)
Learn more about [GitHub Pages on its website](https://pages.github.com/)
## Advanced
All of these options, while using providers with more service offerings,
demonstrate how to host the viewer with their respective file storage and CDN
offerings. While you can serve your static assets this way, if you're going
through the trouble of using AWS/GCP/Azure, it's more likely you're doing so to
avoid using a proxy or to simplify authentication.
If that is the case, check out some of our more advanced `docker` deployments
that target these providers from the left-hand sidepanel.
These guides can be a bit longer and a update more frequently. To provide
accurate documentation, we will link to each provider's own recommended steps:
### AWS S3 + Cloudfront
- [Host a Static Website](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html)
- [Speed Up Your Website with Cloudfront](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-cloudfront-walkthrough.html)
### GCP + Cloudflare
- [Things to Know Before Getting Started](https://code.luasoftware.com/tutorials/google-cloud-storage/things-to-know-before-hosting-static-website-on-google-cloud-storage/)
- [Hosting a Static Website on GCP](https://cloud.google.com/storage/docs/hosting-static-website)
### Azure
- [Host a Static Website](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website)
- [Add SSL Support](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-https-custom-domain-cdn)
- [Configure a Custom Domain](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-custom-domain-name)
@@ -0,0 +1,292 @@
---
sidebar_position: 5
---
# User Account Control
> DISCLAIMER! We make no claims or guarantees of this approach's security. If in
> doubt, enlist the help of an expert and conduct proper audits.
Making a viewer and its medical imaging data accessible on the open web can
provide a lot of benefits, but requires additional security to make sure
sensitive information can only be viewed by authorized individuals. Most image
archives are equipped with basic security measures, but they are not
robust/secure enough for the open web.
This guide covers one of many potential production setups that secure our
sensitive data.
## Overview
This guide builds on top of our
[Nginx + Image Archive guide](/deployment/recipes/nginx--image-archive.md),
wherein we used a [`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy)
to retrieve resources from our image archive (Orthanc).
To add support for "User Account Control" we introduce
[Keycloak](https://www.keycloak.org/about.html). Keycloak is an open source
Identity and Access Management solution that makes it easy to secure
applications and services with little to no code. We improve upon our
`reverse proxy` setup by integrating Keycloak and Nginx to create an
`authenticating reverse proxy`.
> An authenticating reverse proxy is a reverse proxy that only retrieves the
> resources on behalf of a client if the client has been authenticated. If a
> client is not authenticated they can be redirected to a login page.
This setup allows us to create a setup similar to the one pictured below:
![userControlFlow](../../assets/img/user-access-control-request-flow.png)
- All web requests are routed through `nginx` on our `OpenResty` image
- `/pacs` is a reverse proxy for `orthanc`'s `DICOM Web` endpoints
- Requires valid `Authorization: Bearer <token>` header
- `/pacs-admin` is a reverse proxy for `orthanc`'s Web Admin
- `/auth` is a reverse proxy for `keycloak`
- All static resources for OHIF Viewer are unprotected and accessible. We have
application logic that will redirect unauthenticated users to the appropriate
`keycloak` login screen.
## Getting Started
### Requirements
- Docker
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
- [Docker for Windows](https://docs.docker.com/docker-for-windows/)
_Not sure if you have `docker` installed already? Try running `docker --version`
in command prompt or terminal_
### Setup
_Spin Things Up_
- Navigate to `<project-root>/docker/OpenResty-Orthanc-Keycloak` in your shell
- Run `docker-compose up`
_Create Your First User_
- Navigate to: `http://127.0.0.1/auth/admin`
- Sign in with: `admin`/`password`
- From the top left dropdown, select the `Ohif` realm
- From the left sidebar, under `Manage`, select `Users`
- Click `Add User`
- Username: `test`
- Email Verified: `ON`
- Click `Save`
- Click the `Credentials` Tab
- New Pasword: `test`
- Password Confirmation: `test`
- Temporary: `OFF`
- Click: `Reset Password`
- From the top right dropdown, select `Admin`, then `Sign Out`
_Sign In_
- Navigate to `http://127.0.0.1/`
- Username: `test`, Password: `test`
- Click `Log In`
_Upload Your First Study_
- Navigate to `http://127.0.0.1/pacs-admin`
- If you're not already logged in, use `test`/`test`
- From the top right, select "Upload"
- Click "Select files to upload..." (DICOM)
- Click "Start the upload"
- Navigate back to `http://127.0.0.1/` to view your studies in the Study List
### Troubleshooting
_Exit code 137_
This means Docker ran out of memory. Open Docker Desktop, go to the `advanced`
tab, and increase the amount of Memory available.
_Cannot create container for service X_
Use this one with caution: `docker system prune`
_X is already running_
Stop running all containers:
- Win: `docker ps -a -q | ForEach { docker stop $_ }`
- Linux: `docker stop $(docker ps -a -q)`
### Configuration
After verifying that everything runs with default configuration values, you will
likely want to update:
- The domain: `http://127.0.0.1`
- Set secure, non-default passwords
- Regenerate Keycloak Client Secrets
#### OHIF Viewer
The OHIF Viewer's configuration is imported from a static `.js` file. The
configuration we use is set to a specific file when we build the viewer, and
determined by the env variable: `APP_CONFIG`. You can see where we set its value
in the `dockerfile` for this solution:
`ENV APP_CONFIG=config/docker_openresty-orthanc-keycloak.js`
You can find the configuration we're using here:
`/public/config/docker_openresty-orthanc-keycloak.js`
To rebuild the `webapp` image created by our `dockerfile` after updating the
Viewer's configuration, you can run:
- `docker-compose build` OR
- `docker-compose up --build`
#### Other
All other files are found in: `/docker/OpenResty-Orthanc-Keycloak/`
| Service | Configuration | Docs |
| ----------------- | ------------------------------------------------ | ------------------------------------------- |
| OHIF Viewer | [dockerfile][dockerfile] / [config.js][config] | You're reading them now! |
| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] |
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
| Keycloak | [`/ohif-keycloak-realm.json`][config-keycloak]\* | |
\* These are the seed values for Keycloak. They can be manually updated at
`http://127.0.0.1/auth/admin`
#### Keycloak Themeing
The `Login` screen for the `ohif-viewer` client is using a Custom Keycloak
theme. You can find the source files for it in
`/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/`. You can see how
we add it to Keycloak in the `docker-compose` file, and you can read up on how
to leverage custom themes in
[Keycloak's own docs](https://www.keycloak.org/docs/latest/server_development/index.html#_themes).
| Default Theme | OHIF Theme |
| ---------------------------------------------------------------------- | ---------------------------------------------------------------- |
| ![Keycloak Default Theme](../../assets/img/keycloak-default-theme.png) | ![Keycloak OHIF Theme](../../assets/img/keycloak-ohif-theme.png) |
## Next Steps
### Deploying to Production
While these configuration and docker-compose files model an environment suitable
for production, they are not easy to deploy "as is". You can either:
- Manually recreate this environment and deploy built application files **OR**
- Deploy to a cloud kubernetes provider like
[Digital Ocean](https://www.digitalocean.com/products/kubernetes/) **OR**
- [See a full list of cloud providers here](https://landscape.cncf.io/category=cloud&format=card-mode&grouping=category)
- Find and follow your preferred provider's guide on setting up
[swarms and stacks](https://docs.docker.com/get-started/)
### Adding SSL
Adding SSL registration and renewal for your domain with Let's Encrypt that
terminates at Nginx is an incredibly important step toward securing your data.
Here are some resources, specific to this setup, that may be helpful:
- [lua-resty-auto-ssl](https://github.com/GUI/lua-resty-auto-ssl)
- [Let's Encrypt + Nginx](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/)
While we terminate SSL at Nginx, it may be worth using self signed certificates
for communication between services.
- [SSL Termination for TCP Upstream Servers](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-tcp/)
### Use PostgresSQL w/ Orthanc
Orthanc can handle a large amount of data and requests, but if you find that
requests start to slow as you add more and more studies, you may want to
configure your Orthanc instance to use PostgresSQL. Instructions on how to do
that can be found in the
[`Orthanc Server Book`](http://book.orthanc-server.com/users/docker.html), under
"PostgreSQL and Orthanc inside Docker"
### Improving This Guide
Here are some improvements this guide would benefit from, and that we would be
more than happy to accept Pull Requests for:
- SSL Support
- Complete configuration with `.env` file (or something similar)
- Keycloak Theme improvements
- Any security issues
- One-click deploy to a cloud provider
## Resources
### Misc. Helpful Commands
_Check if `nginx.conf` is valid:_
```bash
docker run --rm -t -a stdout --name my-openresty -v $PWD/config/:/usr/local/openresty/nginx/conf/:ro openresty/openresty:alpine-fat openresty -c /usr/local/openresty/nginx/conf/nginx.conf -t
```
_Interact w/ running container:_
`docker exec -it CONTAINER_NAME bash`
_List running containers:_
`docker ps`
_Clear Keycloak DB so you can re-seed values:_
- `docker volume prune` OR
- `docker volume ls` and `docker volume rm VOLUME_NAME VOLUME_NAME`
### Referenced Articles
The inspiration for our setup was driven largely by these articles:
- [Securing Nginx with Keycloak](https://edhull.co.uk/blog/2018-06-06/keycloak-nginx)
- [Authenticating Reverse Proxy with Keycloak](https://eclipsesource.com/blogs/2018/01/11/authenticating-reverse-proxy-with-keycloak/)
- [Securing APIs with Kong and Keycloak](https://www.jerney.io/secure-apis-kong-keycloak-1/)
For more documentation on the software we've chosen to use, you may find the
following resources helpful:
- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html)
- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/)
- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/)
- [Auth0: Picking a Grant Type](https://auth0.com/docs/api-auth/which-oauth-flow-to-use)
We chose to use a generic OpenID Connect library on the client, but it's worth
noting that Keycloak comes packaged with its own:
- [oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki)
- [Keycloak JavaScript Adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
If you're not already drowning in links, here are some good security resources
for OAuth:
- [Diagrams of OpenID Connect Flows](https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660)
- [KeyCloak: OpenID Connect Flows](https://www.keycloak.org/docs/latest/securing_apps/index.html#authorization-code)
For a different take on this setup, check out the repositories our community
members put together:
- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker)
- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker)
<!--
Links
-->
<!-- prettier-ignore-start -->
<!-- DOCS -->
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
<!-- SRC -->
[config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/src/config.js
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/dockerfile
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/nginx.conf
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/orthanc.json
[config-keycloak]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/ohif-keycloak-realm.json
<!-- prettier-ignore-end -->