feat: Support for OpenID Connect (#2431)
* feat: Add OpenID Connect support, speed up docker rebuilds * fix: Switch Google Cloud API URL to v1 * chore: Remove redux-oidc and use our own components instead
This commit is contained in:
parent
5643f8f6d2
commit
e7b32cc51e
@ -21,7 +21,7 @@ if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
|
|||||||
echo "Updating config..."
|
echo "Updating config..."
|
||||||
|
|
||||||
# - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js
|
# - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js
|
||||||
sed -i -e "s+https://healthcare.googleapis.com/v1beta1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js
|
sed -i -e "s+https://healthcare.googleapis.com/v1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# - Copy google.js to overwrite app-config.js
|
# - Copy google.js to overwrite app-config.js
|
||||||
|
|||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -26,6 +26,7 @@
|
|||||||
"autoFix": true
|
"autoFix": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"jest.autoRun": "off",
|
||||||
"prettier.disableLanguages": ["html"],
|
"prettier.disableLanguages": ["html"],
|
||||||
"prettier.endOfLine": "lf",
|
"prettier.endOfLine": "lf",
|
||||||
"workbench.colorCustomizations": {},
|
"workbench.colorCustomizations": {},
|
||||||
|
|||||||
@ -101,7 +101,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
new UnusedFilesWebpackPlugin({
|
new UnusedFilesWebpackPlugin({
|
||||||
failOnUnused: true,
|
failOnUnused: false,
|
||||||
globOptions: {
|
globOptions: {
|
||||||
ignore: [
|
ignore: [
|
||||||
"node_modules/**/*",
|
"node_modules/**/*",
|
||||||
|
|||||||
35
Dockerfile
35
Dockerfile
@ -21,28 +21,37 @@
|
|||||||
|
|
||||||
# Stage 1: Build the application
|
# Stage 1: Build the application
|
||||||
# docker build -t ohif/viewer:latest .
|
# docker build -t ohif/viewer:latest .
|
||||||
FROM node:14.3.0-slim as builder
|
FROM node:14.3.0-slim as json-copier
|
||||||
|
|
||||||
RUN mkdir /usr/src/app
|
RUN mkdir /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
# Copy Files
|
|
||||||
COPY .docker /usr/src/app/.docker
|
COPY ["package.json", "yarn.lock", "./"]
|
||||||
COPY .webpack /usr/src/app/.webpack
|
|
||||||
COPY extensions /usr/src/app/extensions
|
COPY extensions /usr/src/app/extensions
|
||||||
COPY modes /usr/src/app/modes
|
COPY modes /usr/src/app/modes
|
||||||
COPY platform /usr/src/app/platform
|
COPY platform /usr/src/app/platform
|
||||||
COPY .browserslistrc /usr/src/app/.browserslistrc
|
|
||||||
COPY aliases.config.js /usr/src/app/aliases.config.js
|
# Find and remove non-package.json files
|
||||||
COPY babel.config.js /usr/src/app/babel.config.js
|
RUN find extensions \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||||
COPY lerna.json /usr/src/app/lerna.json
|
RUN find modes \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||||
COPY package.json /usr/src/app/package.json
|
RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||||
COPY postcss.config.js /usr/src/app/postcss.config.js
|
|
||||||
COPY yarn.lock /usr/src/app/yarn.lock
|
# Copy Files
|
||||||
|
FROM node:14.3.0-slim as builder
|
||||||
|
RUN mkdir /usr/src/app
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY --from=json-copier /usr/src/app .
|
||||||
|
|
||||||
# Run the install before copying the rest of the files
|
# Run the install before copying the rest of the files
|
||||||
RUN yarn config set workspaces-experimental true
|
RUN yarn config set workspaces-experimental true
|
||||||
RUN yarn install
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# To restore workspaces symlinks
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||||
ENV QUICK_BUILD true
|
ENV QUICK_BUILD true
|
||||||
@ -51,7 +60,7 @@ ENV QUICK_BUILD true
|
|||||||
|
|
||||||
RUN yarn run build
|
RUN yarn run build
|
||||||
|
|
||||||
# Stage 2: Bundle the built application into a Docker container
|
# Stage 3: Bundle the built application into a Docker container
|
||||||
# which runs Nginx using Alpine Linux
|
# which runs Nginx using Alpine Linux
|
||||||
FROM nginx:1.15.5-alpine
|
FROM nginx:1.15.5-alpine
|
||||||
RUN apk add --no-cache bash
|
RUN apk add --no-cache bash
|
||||||
|
|||||||
@ -255,8 +255,7 @@ following resources helpful:
|
|||||||
We chose to use a generic OpenID Connect library on the client, but it's worth
|
We chose to use a generic OpenID Connect library on the client, but it's worth
|
||||||
noting that Keycloak comes packaged with its own:
|
noting that Keycloak comes packaged with its own:
|
||||||
|
|
||||||
- [redux-oidc](https://github.com/maxmantz/redux-oidc) (Which wraps
|
- [oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki)
|
||||||
[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)
|
- [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
|
If you're not already drowning in links, here are some good security resources
|
||||||
|
|||||||
@ -57,7 +57,7 @@ function ExampleContextProvider({ children }) {
|
|||||||
return (
|
return (
|
||||||
<ExampleContext.Provider value={{ example: 'value' }}>
|
<ExampleContext.Provider value={{ example: 'value' }}>
|
||||||
{children}
|
{children}
|
||||||
</TrackedMeasurementsContext.Provider>
|
</ExampleContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -63,9 +63,8 @@ All use the `ACTIVE_VIEWPORT::CORNERSTONE` context.
|
|||||||
|
|
||||||
## Viewport Module
|
## Viewport Module
|
||||||
|
|
||||||
Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport]
|
Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport].
|
||||||
and is connected the redux store. This module is the most prone to change as we
|
This module is the most prone to change as we hammer out our Viewport interface.
|
||||||
hammer out our Viewport interface.
|
|
||||||
|
|
||||||
## Tool Configuration
|
## Tool Configuration
|
||||||
|
|
||||||
@ -87,10 +86,12 @@ Tools can be configured through extension configuration using the tools key:
|
|||||||
|
|
||||||
## Annotate Tools Configuration
|
## Annotate Tools Configuration
|
||||||
|
|
||||||
*We currently support one property for annotation tools.*
|
_We currently support one property for annotation tools._
|
||||||
|
|
||||||
### Hide handles
|
### Hide handles
|
||||||
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
|
|
||||||
|
This extension configuration allows you to toggle on/off handle rendering for
|
||||||
|
all annotate tools:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
...
|
...
|
||||||
@ -119,3 +120,4 @@ This extension configuration allows you to toggle on/off handle rendering for al
|
|||||||
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
|
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
|
||||||
[cornerstone]: https://github.com/cornerstonejs/cornerstone
|
[cornerstone]: https://github.com/cornerstonejs/cornerstone
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
```
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import csTools from 'cornerstone-tools';
|
|||||||
import merge from 'lodash.merge';
|
import merge from 'lodash.merge';
|
||||||
import getTools, { toolsGroupedByType } from './utils/getTools.js';
|
import getTools, { toolsGroupedByType } from './utils/getTools.js';
|
||||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||||
import './initWADOImageLoader.js';
|
import initWADOImageLoader from './initWADOImageLoader.js';
|
||||||
import getCornerstoneMeasurementById from './utils/getCornerstoneMeasurementById';
|
import getCornerstoneMeasurementById from './utils/getCornerstoneMeasurementById';
|
||||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||||
import { setEnabledElement } from './state';
|
import { setEnabledElement } from './state';
|
||||||
@ -69,6 +69,7 @@ export default function init({
|
|||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
ToolBarService,
|
ToolBarService,
|
||||||
|
UserAuthenticationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
const tools = getTools();
|
const tools = getTools();
|
||||||
|
|
||||||
@ -275,6 +276,8 @@ export default function init({
|
|||||||
|
|
||||||
cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
|
cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
|
||||||
|
|
||||||
|
initWADOImageLoader(UserAuthenticationService);
|
||||||
|
|
||||||
// ~~
|
// ~~
|
||||||
const defaultCsToolsConfig = csToolsConfig || {
|
const defaultCsToolsConfig = csToolsConfig || {
|
||||||
globalToolSyncEnabled: false, // hold on to your pants!
|
globalToolSyncEnabled: false, // hold on to your pants!
|
||||||
|
|||||||
@ -2,17 +2,40 @@ import cornerstone from 'cornerstone-core';
|
|||||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import dicomParser from 'dicom-parser';
|
import dicomParser from 'dicom-parser';
|
||||||
|
|
||||||
//import { initWebWorkers } from './utils/index.js';
|
let initialized = false;
|
||||||
|
|
||||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
function initWebWorkers() {
|
||||||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
const config = {
|
||||||
|
maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1),
|
||||||
|
startWebWorkersOnDemand: true,
|
||||||
|
taskConfiguration: {
|
||||||
|
decodeTask: {
|
||||||
|
initializeCodecsOnStartup: false,
|
||||||
|
usePDFJS: false,
|
||||||
|
strict: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
cornerstoneWADOImageLoader.configure({
|
if (!initialized) {
|
||||||
beforeSend: function(xhr) {
|
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
|
||||||
/*const headers = OHIF.DICOMWeb.getAuthorizationHeader();
|
initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (headers.Authorization) {
|
export default function initWADOImageLoader(UserAuthenticationService) {
|
||||||
xhr.setRequestHeader('Authorization', headers.Authorization);
|
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||||
}*/
|
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
||||||
},
|
|
||||||
});
|
cornerstoneWADOImageLoader.configure({
|
||||||
|
beforeSend: function(xhr) {
|
||||||
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
|
|
||||||
|
if (headers && headers.Authorization) {
|
||||||
|
xhr.setRequestHeader('Authorization', headers.Authorization);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
initWebWorkers();
|
||||||
|
}
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
import { redux } from '@ohif/core';
|
|
||||||
|
|
||||||
const { setLayout } = redux.actions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the current layout with a simple Cornerstone one
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
const setCornerstoneLayout = () => {
|
|
||||||
const layout = {
|
|
||||||
numRows: 1,
|
|
||||||
numColumns: 1,
|
|
||||||
viewports: [{ plugin: 'cornerstone' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const action = setLayout(layout);
|
|
||||||
|
|
||||||
window.store.dispatch(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default setCornerstoneLayout;
|
|
||||||
@ -38,7 +38,7 @@ const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
|
|||||||
* @param {bool} supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE)
|
* @param {bool} supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE)
|
||||||
* @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
|
* @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
|
||||||
*/
|
*/
|
||||||
function createDicomWebApi(dicomWebConfig) {
|
function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||||
const {
|
const {
|
||||||
qidoRoot,
|
qidoRoot,
|
||||||
wadoRoot,
|
wadoRoot,
|
||||||
@ -46,15 +46,17 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
supportsFuzzyMatching,
|
supportsFuzzyMatching,
|
||||||
supportsWildcard,
|
supportsWildcard,
|
||||||
supportsReject,
|
supportsReject,
|
||||||
|
requestOptions,
|
||||||
} = dicomWebConfig;
|
} = dicomWebConfig;
|
||||||
|
|
||||||
const qidoConfig = {
|
const qidoConfig = {
|
||||||
url: qidoRoot,
|
url: qidoRoot,
|
||||||
// headers: DICOMWeb.getAuthorizationHeader(server),
|
headers: UserAuthenticationService.getAuthorizationHeader(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const wadoConfig = {
|
const wadoConfig = {
|
||||||
url: wadoRoot,
|
url: wadoRoot,
|
||||||
|
headers: UserAuthenticationService.getAuthorizationHeader(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO -> Two clients sucks, but its better than 1000.
|
// TODO -> Two clients sucks, but its better than 1000.
|
||||||
@ -78,7 +80,12 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
query: {
|
query: {
|
||||||
studies: {
|
studies: {
|
||||||
mapParams: mapParams.bind(),
|
mapParams: mapParams.bind(),
|
||||||
search: async function (origParams) {
|
search: async function(origParams) {
|
||||||
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
|
if (headers) {
|
||||||
|
qidoDicomWebClient.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
|
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
|
||||||
mapParams(origParams, {
|
mapParams(origParams, {
|
||||||
supportsFuzzyMatching,
|
supportsFuzzyMatching,
|
||||||
@ -98,7 +105,12 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
// mapParams: mapParams.bind(),
|
// mapParams: mapParams.bind(),
|
||||||
search: async function (studyInstanceUid) {
|
search: async function(studyInstanceUid) {
|
||||||
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
|
if (headers) {
|
||||||
|
qidoDicomWebClient.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
const results = await seriesInStudy(
|
const results = await seriesInStudy(
|
||||||
qidoDicomWebClient,
|
qidoDicomWebClient,
|
||||||
studyInstanceUid
|
studyInstanceUid
|
||||||
@ -109,14 +121,20 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
// processResults: processResults.bind(),
|
// processResults: processResults.bind(),
|
||||||
},
|
},
|
||||||
instances: {
|
instances: {
|
||||||
search: (studyInstanceUid, queryParameters) =>
|
search: (studyInstanceUid, queryParameters) => {
|
||||||
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
|
if (headers) {
|
||||||
|
qidoDicomWebClient.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
qidoSearch.call(
|
qidoSearch.call(
|
||||||
undefined,
|
undefined,
|
||||||
qidoDicomWebClient,
|
qidoDicomWebClient,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
null,
|
null,
|
||||||
queryParameters
|
queryParameters
|
||||||
),
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
retrieve: {
|
retrieve: {
|
||||||
@ -125,6 +143,11 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
// Conduct query, return a promise like others
|
// Conduct query, return a promise like others
|
||||||
// Await this call and add to DicomMetadataStore after receiving result
|
// Await this call and add to DicomMetadataStore after receiving result
|
||||||
metadata: (queryParams, callback) => {
|
metadata: (queryParams, callback) => {
|
||||||
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
|
if (headers) {
|
||||||
|
wadoDicomWebClient.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
let { StudyInstanceUIDs } = urlUtil.parse(queryParams, true);
|
let { StudyInstanceUIDs } = urlUtil.parse(queryParams, true);
|
||||||
|
|
||||||
StudyInstanceUIDs = urlUtil.paramString.parseParam(StudyInstanceUIDs);
|
StudyInstanceUIDs = urlUtil.paramString.parseParam(StudyInstanceUIDs);
|
||||||
@ -164,6 +187,11 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
},
|
},
|
||||||
store: {
|
store: {
|
||||||
dicom: async dataset => {
|
dicom: async dataset => {
|
||||||
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
|
if (headers) {
|
||||||
|
wadoDicomWebClient.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
FileMetaInformationVersion:
|
FileMetaInformationVersion:
|
||||||
dataset._meta.FileMetaInformationVersion.Value,
|
dataset._meta.FileMetaInformationVersion.Value,
|
||||||
@ -196,6 +224,11 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
sortFunction,
|
sortFunction,
|
||||||
madeInClient = false,
|
madeInClient = false,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
|
if (headers) {
|
||||||
|
wadoDicomWebClient.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
if (!StudyInstanceUID) {
|
if (!StudyInstanceUID) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Unable to query for SeriesMetadata without StudyInstanceUID'
|
'Unable to query for SeriesMetadata without StudyInstanceUID'
|
||||||
|
|||||||
@ -2,7 +2,9 @@ async function getStudiesForPatientByStudyInstanceUID(
|
|||||||
dataSource,
|
dataSource,
|
||||||
StudyInstanceUID
|
StudyInstanceUID
|
||||||
) {
|
) {
|
||||||
if (StudyInstanceUID === undefined) return;
|
if (StudyInstanceUID === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
|
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
|
||||||
// Data _could_ be here from route query, or if using JSON data source
|
// Data _could_ be here from route query, or if using JSON data source
|
||||||
// We could also force this to "await" these values being available in the DICOMStore?
|
// We could also force this to "await" these values being available in the DICOMStore?
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
const HelloWorldContext = React.createContext({
|
|
||||||
message: 'HelloWorldContextTesting',
|
|
||||||
setMessage: () => { },
|
|
||||||
});
|
|
||||||
|
|
||||||
HelloWorldContext.displayName = 'HelloWorldContext';
|
|
||||||
|
|
||||||
function HelloWorldContextProvider({ children }) {
|
|
||||||
const [message, setMessage] = useState('HelloWorldContextTesting');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HelloWorldContext.Provider
|
|
||||||
value={{
|
|
||||||
message,
|
|
||||||
setMessage,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</HelloWorldContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getContextModule() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'HelloWorldContext',
|
|
||||||
context: HelloWorldContext,
|
|
||||||
provider: HelloWorldContextProvider,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export { HelloWorldContext };
|
|
||||||
|
|
||||||
export default getContextModule;
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import getContextModule from './getContextModule.js';
|
|
||||||
import getDataSourcesModule from './getDataSourcesModule.js';
|
import getDataSourcesModule from './getDataSourcesModule.js';
|
||||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||||
import getPanelModule from './getPanelModule.js';
|
import getPanelModule from './getPanelModule.js';
|
||||||
@ -13,7 +12,6 @@ export default {
|
|||||||
* Only required property. Should be a unique value across all extensions.
|
* Only required property. Should be a unique value across all extensions.
|
||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
getContextModule,
|
|
||||||
getDataSourcesModule,
|
getDataSourcesModule,
|
||||||
getHangingProtocolModule,
|
getHangingProtocolModule,
|
||||||
getLayoutTemplateModule,
|
getLayoutTemplateModule,
|
||||||
|
|||||||
@ -2,7 +2,9 @@ async function getStudiesForPatientByStudyInstanceUID(
|
|||||||
dataSource,
|
dataSource,
|
||||||
StudyInstanceUID
|
StudyInstanceUID
|
||||||
) {
|
) {
|
||||||
if (StudyInstanceUID === undefined) return;
|
if (StudyInstanceUID === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
|
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
|
||||||
// Data _could_ be here from route query, or if using JSON data source
|
// Data _could_ be here from route query, or if using JSON data source
|
||||||
// We could also force this to "await" these values being available in the DICOMStore?
|
// We could also force this to "await" these values being available in the DICOMStore?
|
||||||
|
|||||||
@ -43,3 +43,7 @@
|
|||||||
no-cache,
|
no-cache,
|
||||||
no-store,
|
no-store,
|
||||||
must-revalidate'''
|
must-revalidate'''
|
||||||
|
|
||||||
|
# COMMENT: For sharedArrayBuffer, see https://developer.chrome.com/blog/enabling-shared-array-buffer/
|
||||||
|
Cross-Origin-Embedder-Policy = "require-corp"
|
||||||
|
Cross-Origin-Opener-Policy = "same-origin"
|
||||||
|
|||||||
@ -41,6 +41,7 @@
|
|||||||
"dicomweb-client": "^0.6.0",
|
"dicomweb-client": "^0.6.0",
|
||||||
"isomorphic-base64": "^1.0.2",
|
"isomorphic-base64": "^1.0.2",
|
||||||
"lodash.merge": "^4.6.1",
|
"lodash.merge": "^4.6.1",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"mousetrap": "^1.6.3",
|
"mousetrap": "^1.6.3",
|
||||||
"query-string": "^6.14.0",
|
"query-string": "^6.14.0",
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import user from '../user';
|
|||||||
* @param {string|function} [server.requestOptions.auth]
|
* @param {string|function} [server.requestOptions.auth]
|
||||||
* @returns {Object} { Authorization }
|
* @returns {Object} { Authorization }
|
||||||
*/
|
*/
|
||||||
export default function getAuthorizationHeader({ requestOptions } = {}) {
|
export default function getAuthorizationHeader({ requestOptions } = {}, user) {
|
||||||
const headers = {};
|
const headers = {};
|
||||||
|
|
||||||
// Check for OHIF.user since this can also be run on the server
|
// Check for OHIF.user since this can also be run on the server
|
||||||
|
|||||||
@ -64,11 +64,11 @@ describe('getAuthorizationHeader', () => {
|
|||||||
it('should return an Authorization with accessToken when server is not defined and there is an accessToken', () => {
|
it('should return an Authorization with accessToken when server is not defined and there is an accessToken', () => {
|
||||||
user.getAccessToken.mockImplementationOnce(() => 'MOCKED_TOKEN');
|
user.getAccessToken.mockImplementationOnce(() => 'MOCKED_TOKEN');
|
||||||
|
|
||||||
const authentication = getAuthorizationHeader({});
|
const authentication = getAuthorizationHeader({}, user);
|
||||||
const exptecteHeaderBasedOnUserAccessToekn = {
|
const expectedHeaderBasedOnUserAccessToken = {
|
||||||
Authorization: 'Bearer MOCKED_TOKEN',
|
Authorization: 'Bearer MOCKED_TOKEN',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(authentication).toEqual(exptecteHeaderBasedOnUserAccessToekn);
|
expect(authentication).toEqual(expectedHeaderBasedOnUserAccessToken);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -439,7 +439,7 @@ class MetadataProvider {
|
|||||||
|
|
||||||
_getUIDsFromImageID(imageId) {
|
_getUIDsFromImageID(imageId) {
|
||||||
if (imageId.includes('wadors:')) {
|
if (imageId.includes('wadors:')) {
|
||||||
const strippedImageId = imageId.split('studies/')[1];
|
const strippedImageId = imageId.split('/studies/')[1];
|
||||||
const splitImageId = strippedImageId.split('/');
|
const splitImageId = strippedImageId.split('/');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -277,13 +277,16 @@ export default class ExtensionManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_initDataSourcesModule(extensionModule, extensionId, dataSources = []) {
|
_initDataSourcesModule(extensionModule, extensionId, dataSources = []) {
|
||||||
|
const { UserAuthenticationService } = this._servicesManager.services;
|
||||||
|
|
||||||
extensionModule.forEach(element => {
|
extensionModule.forEach(element => {
|
||||||
const namespace = `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`;
|
const namespace = `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`;
|
||||||
|
|
||||||
dataSources.forEach(dataSource => {
|
dataSources.forEach(dataSource => {
|
||||||
if (dataSource.namespace === namespace) {
|
if (dataSource.namespace === namespace) {
|
||||||
const dataSourceInstance = element.createDataSource(
|
const dataSourceInstance = element.createDataSource(
|
||||||
dataSource.configuration
|
dataSource.configuration,
|
||||||
|
UserAuthenticationService
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.dataSourceMap[dataSource.sourceName]) {
|
if (this.dataSourceMap[dataSource.sourceName]) {
|
||||||
|
|||||||
@ -15,6 +15,10 @@ describe('ExtensionManager.js', () => {
|
|||||||
};
|
};
|
||||||
servicesManager = {
|
servicesManager = {
|
||||||
registerService: jest.fn(),
|
registerService: jest.fn(),
|
||||||
|
services: {
|
||||||
|
// Required for DataSource Module initiation
|
||||||
|
UserAuthenticationService: jest.fn(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
appConfig = {
|
appConfig = {
|
||||||
testing: true,
|
testing: true,
|
||||||
@ -100,7 +104,6 @@ describe('ExtensionManager.js', () => {
|
|||||||
extensionManager.registerExtension(undefinedExtension);
|
extensionManager.registerExtension(undefinedExtension);
|
||||||
}).toThrow('Attempting to register a null/undefined extension.');
|
}).toThrow('Attempting to register a null/undefined extension.');
|
||||||
|
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
extensionManager.registerExtension(nullExtension);
|
extensionManager.registerExtension(nullExtension);
|
||||||
}).toThrow('Attempting to register a null/undefined extension.');
|
}).toThrow('Attempting to register a null/undefined extension.');
|
||||||
@ -185,7 +188,7 @@ describe('ExtensionManager.js', () => {
|
|||||||
hotkeysManager: undefined,
|
hotkeysManager: undefined,
|
||||||
appConfig,
|
appConfig,
|
||||||
configuration: extensionConfiguration,
|
configuration: extensionConfiguration,
|
||||||
extensionManager
|
extensionManager,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -220,7 +223,7 @@ describe('ExtensionManager.js', () => {
|
|||||||
},
|
},
|
||||||
getContextModule: () => {
|
getContextModule: () => {
|
||||||
return [{}];
|
return [{}];
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
extensionManager.registerExtension(extension);
|
extensionManager.registerExtension(extension);
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
pubSubServiceInterface,
|
pubSubServiceInterface,
|
||||||
|
UserAuthenticationService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||||
@ -66,6 +67,7 @@ const OHIF = {
|
|||||||
ToolBarService, // TODO: TYPO
|
ToolBarService, // TODO: TYPO
|
||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
|
UserAuthenticationService,
|
||||||
IWebApiDataSource,
|
IWebApiDataSource,
|
||||||
DicomMetadataStore,
|
DicomMetadataStore,
|
||||||
pubSubServiceInterface,
|
pubSubServiceInterface,
|
||||||
@ -101,6 +103,7 @@ export {
|
|||||||
ToolBarService,
|
ToolBarService,
|
||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
|
UserAuthenticationService,
|
||||||
IWebApiDataSource,
|
IWebApiDataSource,
|
||||||
DicomMetadataStore,
|
DicomMetadataStore,
|
||||||
pubSubServiceInterface,
|
pubSubServiceInterface,
|
||||||
|
|||||||
@ -34,9 +34,10 @@ describe('Top level exports', () => {
|
|||||||
'ToolBarService',
|
'ToolBarService',
|
||||||
'ViewportGridService',
|
'ViewportGridService',
|
||||||
'HangingProtocolService',
|
'HangingProtocolService',
|
||||||
|
'UserAuthenticationService',
|
||||||
'IWebApiDataSource',
|
'IWebApiDataSource',
|
||||||
'DicomMetadataStore',
|
'DicomMetadataStore',
|
||||||
'pubSubServiceInterface'
|
'pubSubServiceInterface',
|
||||||
].sort();
|
].sort();
|
||||||
|
|
||||||
const exports = Object.keys(OHIF).sort();
|
const exports = Object.keys(OHIF).sort();
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
|
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
|
||||||
import sortBy from '../../utils/sortBy.js';
|
import sortBy from '../../utils/sortBy.js';
|
||||||
import ProtocolEngine from './ProtocolEngine';
|
import ProtocolEngine from './ProtocolEngine';
|
||||||
|
|||||||
@ -0,0 +1,94 @@
|
|||||||
|
const name = 'UserAuthenticationService';
|
||||||
|
|
||||||
|
const publicAPI = {
|
||||||
|
name,
|
||||||
|
getState: _getState,
|
||||||
|
setUser: _setUser,
|
||||||
|
getUser: _getUser,
|
||||||
|
getAuthorizationHeader: _getAuthorizationHeader,
|
||||||
|
handleUnauthenticated: _handleUnauthenticated,
|
||||||
|
setServiceImplementation,
|
||||||
|
reset: _reset,
|
||||||
|
set: _set,
|
||||||
|
};
|
||||||
|
|
||||||
|
const serviceImplementation = {
|
||||||
|
_getState: () => console.warn('getState() NOT IMPLEMENTED'),
|
||||||
|
_setUser: () => console.warn('_setUser() NOT IMPLEMENTED'),
|
||||||
|
_getUser: () => console.warn('_setUser() NOT IMPLEMENTED'),
|
||||||
|
_getAuthorizationHeader: () =>
|
||||||
|
console.warn('_getAuthorizationHeader() NOT IMPLEMENTED'),
|
||||||
|
_handleUnauthenticated: () =>
|
||||||
|
console.warn('_handleUnauthenticated() NOT IMPLEMENTED'),
|
||||||
|
_reset: () => console.warn('reset() NOT IMPLEMENTED'),
|
||||||
|
_set: () => console.warn('set() NOT IMPLEMENTED'),
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getState() {
|
||||||
|
return serviceImplementation._getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setUser(user) {
|
||||||
|
return serviceImplementation._setUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getUser() {
|
||||||
|
return serviceImplementation._getUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getAuthorizationHeader() {
|
||||||
|
const user = serviceImplementation._getUser();
|
||||||
|
|
||||||
|
return serviceImplementation._getAuthorizationHeader(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _handleUnauthenticated() {
|
||||||
|
return serviceImplementation._handleUnauthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _set(state) {
|
||||||
|
return serviceImplementation._set(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _reset() {
|
||||||
|
return serviceImplementation._reset({});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setServiceImplementation({
|
||||||
|
getState: getStateImplementation,
|
||||||
|
setUser: setUserImplementation,
|
||||||
|
getUser: getUserImplementation,
|
||||||
|
getAuthorizationHeader: getAuthorizationHeaderImplementation,
|
||||||
|
handleUnauthenticated: handleUnauthenticatedImplementation,
|
||||||
|
reset: resetImplementation,
|
||||||
|
set: setImplementation,
|
||||||
|
}) {
|
||||||
|
if (getStateImplementation) {
|
||||||
|
serviceImplementation._getState = getStateImplementation;
|
||||||
|
}
|
||||||
|
if (setUserImplementation) {
|
||||||
|
serviceImplementation._setUser = setUserImplementation;
|
||||||
|
}
|
||||||
|
if (getUserImplementation) {
|
||||||
|
serviceImplementation._getUser = getUserImplementation;
|
||||||
|
}
|
||||||
|
if (getAuthorizationHeaderImplementation) {
|
||||||
|
serviceImplementation._getAuthorizationHeader = getAuthorizationHeaderImplementation;
|
||||||
|
}
|
||||||
|
if (handleUnauthenticatedImplementation) {
|
||||||
|
serviceImplementation._handleUnauthenticated = handleUnauthenticatedImplementation;
|
||||||
|
}
|
||||||
|
if (resetImplementation) {
|
||||||
|
serviceImplementation._reset = resetImplementation;
|
||||||
|
}
|
||||||
|
if (setImplementation) {
|
||||||
|
serviceImplementation._set = setImplementation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name,
|
||||||
|
create: ({ configuration = {} }) => {
|
||||||
|
return publicAPI;
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import UserAuthenticationService from './UserAuthenticationService';
|
||||||
|
|
||||||
|
export default UserAuthenticationService;
|
||||||
@ -11,6 +11,7 @@ import ViewportGridService from './ViewportGridService';
|
|||||||
import CineService from './CineService';
|
import CineService from './CineService';
|
||||||
import HangingProtocolService from './HangingProtocolService';
|
import HangingProtocolService from './HangingProtocolService';
|
||||||
import pubSubServiceInterface from './_shared/pubSubServiceInterface';
|
import pubSubServiceInterface from './_shared/pubSubServiceInterface';
|
||||||
|
import UserAuthenticationService from './UserAuthenticationService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
@ -26,4 +27,5 @@ export {
|
|||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
CineService,
|
CineService,
|
||||||
pubSubServiceInterface,
|
pubSubServiceInterface,
|
||||||
|
UserAuthenticationService,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,6 +25,9 @@ export {
|
|||||||
ViewportGridContext,
|
ViewportGridContext,
|
||||||
ViewportGridProvider,
|
ViewportGridProvider,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
|
UserAuthenticationContext,
|
||||||
|
UserAuthenticationProvider,
|
||||||
|
useUserAuthentication,
|
||||||
} from './src/contextProviders';
|
} from './src/contextProviders';
|
||||||
|
|
||||||
/** COMPONENTS */
|
/** COMPONENTS */
|
||||||
@ -92,7 +95,7 @@ export {
|
|||||||
ViewportDownloadForm,
|
ViewportDownloadForm,
|
||||||
ViewportGrid,
|
ViewportGrid,
|
||||||
ViewportPane,
|
ViewportPane,
|
||||||
WindowLevelMenuItem
|
WindowLevelMenuItem,
|
||||||
} from './src/components';
|
} from './src/components';
|
||||||
|
|
||||||
/** VIEWS */
|
/** VIEWS */
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
"react-dnd-html5-backend": "10.0.2",
|
"react-dnd-html5-backend": "10.0.2",
|
||||||
"react-dom": "16.11.0",
|
"react-dom": "16.11.0",
|
||||||
"react-draggable": "4.4.3",
|
"react-draggable": "4.4.3",
|
||||||
"react-error-boundary": "2.2.x",
|
"react-error-boundary": "^3.1.3",
|
||||||
"react-modal": "3.11.2",
|
"react-modal": "3.11.2",
|
||||||
"react-outside-click-handler": "^1.3.0",
|
"react-outside-click-handler": "^1.3.0",
|
||||||
"react-select": "3.0.8",
|
"react-select": "3.0.8",
|
||||||
|
|||||||
@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
// TODO: This may fail if package is split from PWA build
|
// TODO: This may fail if package is split from PWA build
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { NavBar, Svg, Icon, IconButton, Dropdown } from '@ohif/ui';
|
import { NavBar, Svg, Icon, IconButton, Dropdown } from '@ohif/ui';
|
||||||
|
|
||||||
function Header({ children, menuOptions, isReturnEnabled, isSticky, WhiteLabeling }) {
|
function Header({ children, menuOptions, isReturnEnabled, isSticky, WhiteLabeling }) {
|
||||||
const { t } = useTranslation('Header');
|
const { t } = useTranslation('Header');
|
||||||
const history = useHistory();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// TODO: this should be passed in as a prop instead and the react-router-dom
|
// TODO: this should be passed in as a prop instead and the react-router-dom
|
||||||
// dependency should be dropped
|
// dependency should be dropped
|
||||||
const onReturnHandler = () => {
|
const onReturnHandler = () => {
|
||||||
if (isReturnEnabled) {
|
if (isReturnEnabled) {
|
||||||
history.push('/');
|
navigate('/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
133
platform/ui/src/contextProviders/UserAuthenticationProvider.js
Normal file
133
platform/ui/src/contextProviders/UserAuthenticationProvider.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useReducer,
|
||||||
|
} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
user: null,
|
||||||
|
enabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserAuthenticationContext = createContext(DEFAULT_STATE);
|
||||||
|
|
||||||
|
export function UserAuthenticationProvider({ children, service }) {
|
||||||
|
const userAuthenticationReducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_USER': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...{ user: action.payload.user },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'RESET': {
|
||||||
|
return {
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return action.payload;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [userAuthenticationState, dispatch] = useReducer(
|
||||||
|
userAuthenticationReducer,
|
||||||
|
DEFAULT_STATE
|
||||||
|
);
|
||||||
|
|
||||||
|
const getState = useCallback(() => userAuthenticationState, [
|
||||||
|
userAuthenticationState,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const setUser = useCallback(
|
||||||
|
user =>
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_USER',
|
||||||
|
payload: {
|
||||||
|
user,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getUser = useCallback(() => userAuthenticationState.user, [
|
||||||
|
userAuthenticationState,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const reset = useCallback(
|
||||||
|
() =>
|
||||||
|
dispatch({
|
||||||
|
type: 'RESET',
|
||||||
|
payload: {},
|
||||||
|
}),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const set = useCallback(
|
||||||
|
payload =>
|
||||||
|
dispatch({
|
||||||
|
type: 'SET',
|
||||||
|
payload,
|
||||||
|
}),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the implementation of the UserAuthenticationService that can be used by extensions.
|
||||||
|
*
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (service) {
|
||||||
|
service.setServiceImplementation({
|
||||||
|
getState,
|
||||||
|
setUser,
|
||||||
|
getUser,
|
||||||
|
reset,
|
||||||
|
set,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [getState, service, setUser, getUser, reset, set]);
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
getState,
|
||||||
|
setUser,
|
||||||
|
getUser,
|
||||||
|
getAuthorizationHeader: service.getAuthorizationHeader,
|
||||||
|
handleUnauthenticated: service.handleUnauthenticated,
|
||||||
|
reset,
|
||||||
|
set,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserAuthenticationContext.Provider value={[userAuthenticationState, api]}>
|
||||||
|
{children}
|
||||||
|
</UserAuthenticationContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserAuthenticationProvider;
|
||||||
|
|
||||||
|
const UserAuthenticationConsumer = UserAuthenticationContext.Consumer;
|
||||||
|
export { UserAuthenticationConsumer };
|
||||||
|
|
||||||
|
UserAuthenticationProvider.propTypes = {
|
||||||
|
children: PropTypes.any,
|
||||||
|
service: PropTypes.shape({
|
||||||
|
setServiceImplementation: PropTypes.func,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUserAuthentication = () =>
|
||||||
|
useContext(UserAuthenticationContext);
|
||||||
@ -2,7 +2,7 @@ export {
|
|||||||
default as DialogProvider,
|
default as DialogProvider,
|
||||||
useDialog,
|
useDialog,
|
||||||
withDialog,
|
withDialog,
|
||||||
} from './DialogProvider'
|
} from './DialogProvider';
|
||||||
|
|
||||||
export { default as DragAndDropProvider } from './DragAndDropProvider';
|
export { default as DragAndDropProvider } from './DragAndDropProvider';
|
||||||
|
|
||||||
@ -19,17 +19,13 @@ export {
|
|||||||
useImageViewer,
|
useImageViewer,
|
||||||
} from './ImageViewerProvider';
|
} from './ImageViewerProvider';
|
||||||
|
|
||||||
export {
|
export { CineContext, default as CineProvider, useCine } from './CineProvider';
|
||||||
CineContext,
|
|
||||||
default as CineProvider,
|
|
||||||
useCine,
|
|
||||||
} from './CineProvider';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
default as SnackbarProvider,
|
default as SnackbarProvider,
|
||||||
useSnackbar,
|
useSnackbar,
|
||||||
withSnackbar,
|
withSnackbar,
|
||||||
} from './SnackbarProvider'
|
} from './SnackbarProvider';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
default as ViewportDialogProvider,
|
default as ViewportDialogProvider,
|
||||||
@ -41,3 +37,9 @@ export {
|
|||||||
ViewportGridProvider,
|
ViewportGridProvider,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
} from './ViewportGridProvider';
|
} from './ViewportGridProvider';
|
||||||
|
|
||||||
|
export {
|
||||||
|
UserAuthenticationContext,
|
||||||
|
UserAuthenticationProvider,
|
||||||
|
useUserAuthentication,
|
||||||
|
} from './UserAuthenticationProvider';
|
||||||
|
|||||||
@ -112,6 +112,10 @@ module.exports = (env, argv) => {
|
|||||||
historyApiFallback: {
|
historyApiFallback: {
|
||||||
disableDotRule: true,
|
disableDotRule: true,
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -62,17 +62,19 @@
|
|||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"dotenv-webpack": "^1.7.0",
|
"dotenv-webpack": "^1.7.0",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
|
"history": "5.0.0",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
"lodash.isequal": "4.5.0",
|
"lodash.isequal": "4.5.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
|
"oidc-client": "1.11.5",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"query-string": "^6.12.1",
|
"query-string": "^6.12.1",
|
||||||
"react-dropzone": "^10.1.7",
|
"react-dropzone": "^10.1.7",
|
||||||
"react-i18next": "^10.11.0",
|
"react-i18next": "^10.11.0",
|
||||||
"react-resize-detector": "^4.2.0",
|
"react-resize-detector": "^4.2.0",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "next",
|
||||||
"react-router-dom": "^5.2.0"
|
"react-router-dom": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@percy/cypress": "^2.3.0",
|
"@percy/cypress": "^2.3.0",
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { Router } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import Compose from './routes/Mode/Compose.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DialogProvider,
|
DialogProvider,
|
||||||
Modal,
|
Modal,
|
||||||
@ -13,22 +15,18 @@ import {
|
|||||||
ViewportDialogProvider,
|
ViewportDialogProvider,
|
||||||
ViewportGridProvider,
|
ViewportGridProvider,
|
||||||
CineProvider,
|
CineProvider,
|
||||||
|
UserAuthenticationProvider,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
// Viewer Project
|
// Viewer Project
|
||||||
// TODO: Should this influence study list?
|
// TODO: Should this influence study list?
|
||||||
import { AppConfigProvider } from '@state';
|
import { AppConfigProvider } from '@state';
|
||||||
import createRoutes from './routes';
|
import createRoutes from './routes';
|
||||||
import appInit from './appInit.js';
|
import appInit from './appInit.js';
|
||||||
import history from './history'
|
import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes.jsx';
|
||||||
|
|
||||||
// TODO: Temporarily for testing
|
// TODO: Temporarily for testing
|
||||||
import '@ohif/mode-longitudinal';
|
import '@ohif/mode-longitudinal';
|
||||||
|
|
||||||
/**
|
|
||||||
* ENV Variable to determine routing behavior
|
|
||||||
*/
|
|
||||||
const OHIFRouter = Router
|
|
||||||
|
|
||||||
let commandsManager, extensionManager, servicesManager, hotkeysManager;
|
let commandsManager, extensionManager, servicesManager, hotkeysManager;
|
||||||
|
|
||||||
function App({ config, defaultExtensions }) {
|
function App({ config, defaultExtensions }) {
|
||||||
@ -42,7 +40,8 @@ function App({ config, defaultExtensions }) {
|
|||||||
|
|
||||||
// Set appConfig
|
// Set appConfig
|
||||||
const appConfigState = init.appConfig;
|
const appConfigState = init.appConfig;
|
||||||
const { routerBasename, modes, dataSources } = appConfigState;
|
const { routerBasename, modes, dataSources, oidc } = appConfigState;
|
||||||
|
|
||||||
// Use config to create routes
|
// Use config to create routes
|
||||||
const appRoutes = createRoutes({
|
const appRoutes = createRoutes({
|
||||||
modes,
|
modes,
|
||||||
@ -50,38 +49,52 @@ function App({ config, defaultExtensions }) {
|
|||||||
extensionManager,
|
extensionManager,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
hotkeysManager,
|
hotkeysManager,
|
||||||
|
routerBasename,
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
ViewportGridService, // TODO: Should this be a "UI" Service?
|
ViewportGridService,
|
||||||
CineService
|
CineService,
|
||||||
|
UserAuthenticationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
const providers = [
|
||||||
|
[AppConfigProvider, { value: appConfigState }],
|
||||||
|
[UserAuthenticationProvider, { service: UserAuthenticationService}],
|
||||||
|
[I18nextProvider, { i18n }],
|
||||||
|
[ThemeWrapper],
|
||||||
|
[ViewportGridProvider, {service: ViewportGridService}],
|
||||||
|
[ViewportDialogProvider, {service: UIViewportDialogService}],
|
||||||
|
[CineProvider, {service: CineService}],
|
||||||
|
[SnackbarProvider, {service: UINotificationService}],
|
||||||
|
[DialogProvider, {service: UIDialogService}],
|
||||||
|
[ModalProvider, {service: UIModalService, modal: Modal}],
|
||||||
|
]
|
||||||
|
const CombinedProviders = ({ children }) =>
|
||||||
|
Compose({ components: providers, children });
|
||||||
|
|
||||||
|
let authRoutes = null;
|
||||||
|
|
||||||
|
if (oidc) {
|
||||||
|
UserAuthenticationService.set({ enabled: true });
|
||||||
|
|
||||||
|
authRoutes = (<OpenIdConnectRoutes
|
||||||
|
oidc={oidc}
|
||||||
|
routerBasename={routerBasename}
|
||||||
|
UserAuthenticationService={UserAuthenticationService}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppConfigProvider value={appConfigState}>
|
<CombinedProviders>
|
||||||
<I18nextProvider i18n={i18n}>
|
<BrowserRouter>
|
||||||
<OHIFRouter basename={routerBasename} history={history}>
|
{authRoutes}
|
||||||
<ThemeWrapper>
|
{appRoutes}
|
||||||
<ViewportGridProvider service={ViewportGridService}>
|
</BrowserRouter>
|
||||||
<ViewportDialogProvider service={UIViewportDialogService}>
|
</CombinedProviders>
|
||||||
<CineProvider service={CineService}>
|
|
||||||
<SnackbarProvider service={UINotificationService}>
|
|
||||||
<DialogProvider service={UIDialogService}>
|
|
||||||
<ModalProvider modal={Modal} service={UIModalService}>
|
|
||||||
{appRoutes}
|
|
||||||
</ModalProvider>
|
|
||||||
</DialogProvider>
|
|
||||||
</SnackbarProvider>
|
|
||||||
</CineProvider>
|
|
||||||
</ViewportDialogProvider>
|
|
||||||
</ViewportGridProvider>
|
|
||||||
</ThemeWrapper>
|
|
||||||
</OHIFRouter>
|
|
||||||
</I18nextProvider>
|
|
||||||
</AppConfigProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,11 +13,10 @@ import {
|
|||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
CineService,
|
CineService,
|
||||||
|
UserAuthenticationService,
|
||||||
// utils,
|
// utils,
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
|
|
||||||
// TODO -> this feels bad.
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
|
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
|
||||||
* @param {object[]} defaultExtensions - array of extension objects
|
* @param {object[]} defaultExtensions - array of extension objects
|
||||||
@ -60,6 +59,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
|||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
CineService,
|
CineService,
|
||||||
|
UserAuthenticationService,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,13 +75,16 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
|||||||
// TODO: We no longer init webWorkers at app level
|
// TODO: We no longer init webWorkers at app level
|
||||||
// TODO: We no longer init the user Manager
|
// TODO: We no longer init the user Manager
|
||||||
|
|
||||||
|
if (!appConfig.modes) {
|
||||||
|
throw new Error('No modes are defined! Check your app-config.js');
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove this
|
// TODO: Remove this
|
||||||
if (!appConfig.modes.length) {
|
if (!appConfig.modes.length) {
|
||||||
appConfig.modes.push(window.longitudinalMode);
|
appConfig.modes.push(window.longitudinalMode);
|
||||||
// appConfig.modes.push(window.segmentationMode);
|
// appConfig.modes.push(window.segmentationMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appConfig,
|
appConfig,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// TODO: replace with HistoryRouter https://github.com/ReactTraining/react-router/pull/7586
|
||||||
import { createBrowserHistory, createHashHistory } from 'history';
|
import { createBrowserHistory, createHashHistory } from 'history';
|
||||||
const useHashRouting = JSON.parse(process.env.USE_HASH_ROUTER);
|
const useHashRouting = JSON.parse(process.env.USE_HASH_ROUTER);
|
||||||
const router = useHashRouting ? createHashHistory() : createBrowserHistory();
|
const router = useHashRouting ? createHashHistory() : createBrowserHistory();
|
||||||
|
|||||||
22
platform/viewer/src/routes/CallbackPage.js
Normal file
22
platform/viewer/src/routes/CallbackPage.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function CallbackPage({ userManager, onRedirectSuccess }) {
|
||||||
|
const onRedirectError = error => {
|
||||||
|
throw new Error(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
userManager
|
||||||
|
.signinRedirectCallback()
|
||||||
|
.then(user => onRedirectSuccess(user))
|
||||||
|
.catch(error => onRedirectError(error));
|
||||||
|
|
||||||
|
// todo: add i18n (or return null?)
|
||||||
|
return <div>Redirecting...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallbackPage.propTypes = {
|
||||||
|
userManager: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CallbackPage;
|
||||||
@ -3,8 +3,8 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { MODULE_TYPES } from '@ohif/core';
|
import { MODULE_TYPES } from '@ohif/core';
|
||||||
//
|
//
|
||||||
import { useAppConfig } from '@state';
|
|
||||||
import { extensionManager } from '../App.jsx';
|
import { extensionManager } from '../App.jsx';
|
||||||
|
import { useParams, useLocation } from 'react-router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses route properties to determine the data source that should be passed
|
* Uses route properties to determine the data source that should be passed
|
||||||
@ -15,8 +15,10 @@ import { extensionManager } from '../App.jsx';
|
|||||||
* @param {function} props.children - Layout Template React Component
|
* @param {function} props.children - Layout Template React Component
|
||||||
*/
|
*/
|
||||||
function DataSourceWrapper(props) {
|
function DataSourceWrapper(props) {
|
||||||
const [appConfig] = useAppConfig();
|
const { children: LayoutTemplate, ...rest } = props;
|
||||||
const { children: LayoutTemplate, history, ...rest } = props;
|
const params = useParams();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
// TODO: Fetch by type, name, etc?
|
// TODO: Fetch by type, name, etc?
|
||||||
const dataSourceModules = extensionManager.modules[MODULE_TYPES.DATA_SOURCE];
|
const dataSourceModules = extensionManager.modules[MODULE_TYPES.DATA_SOURCE];
|
||||||
// TODO: Good usecase for flatmap?
|
// TODO: Good usecase for flatmap?
|
||||||
@ -30,13 +32,10 @@ function DataSourceWrapper(props) {
|
|||||||
return acc.concat(mods);
|
return acc.concat(mods);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Grabbing first for now. This isn't hydrated yet, but we should
|
// Grabbing first for now - should get active?
|
||||||
// hydrate it somewhere based on config...
|
const name = webApiDataSources[0].name;
|
||||||
// ~ default.js
|
// TODO: Why does this return an array?
|
||||||
const firstAppConfigDataSource = appConfig.dataSources[0];
|
const dataSource = extensionManager.getDataSources(name)[0]
|
||||||
const dataSourceConfig = firstAppConfigDataSource.configuration;
|
|
||||||
const firstWebApiDataSource = webApiDataSources[0];
|
|
||||||
const dataSource = firstWebApiDataSource.createDataSource(dataSourceConfig);
|
|
||||||
|
|
||||||
// Route props --> studies.mapParams
|
// Route props --> studies.mapParams
|
||||||
// mapParams --> studies.search
|
// mapParams --> studies.search
|
||||||
@ -55,19 +54,18 @@ function DataSourceWrapper(props) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const queryFilterValues = _getQueryFilterValues(
|
const queryFilterValues = _getQueryFilterValues(
|
||||||
history.location.search,
|
location.search,
|
||||||
STUDIES_LIMIT
|
STUDIES_LIMIT
|
||||||
);
|
);
|
||||||
|
|
||||||
// 204: no content
|
// 204: no content
|
||||||
async function getData() {
|
async function getData() {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const studies = await dataSource.query.studies.search(queryFilterValues);
|
const studies = await dataSource.query.studies.search(queryFilterValues);
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setData({
|
setData({
|
||||||
studies,
|
studies: studies || [],
|
||||||
total: studies.length,
|
total: studies.length,
|
||||||
resultsPerPage: queryFilterValues.resultsPerPage,
|
resultsPerPage: queryFilterValues.resultsPerPage,
|
||||||
pageNumber: queryFilterValues.pageNumber,
|
pageNumber: queryFilterValues.pageNumber,
|
||||||
@ -99,14 +97,13 @@ function DataSourceWrapper(props) {
|
|||||||
console.warn(ex);
|
console.warn(ex);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [history.location.search]);
|
}, [location, params]);
|
||||||
// queryFilterValues
|
// queryFilterValues
|
||||||
|
|
||||||
// TODO: Better way to pass DataSource?
|
// TODO: Better way to pass DataSource?
|
||||||
return (
|
return (
|
||||||
<LayoutTemplate
|
<LayoutTemplate
|
||||||
{...rest}
|
{...rest}
|
||||||
history={history}
|
|
||||||
data={data.studies}
|
data={data.studies}
|
||||||
dataTotal={data.total}
|
dataTotal={data.total}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
|
|||||||
@ -10,14 +10,18 @@ export default function Compose(props) {
|
|||||||
const { components = [], children } = props;
|
const { components = [], children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<React.Fragment>
|
||||||
{components.reduceRight((acc, Comp) => {
|
{components.reduceRight((acc, curr) => {
|
||||||
return <Comp>{acc}</Comp>;
|
const [Comp, props] = Array.isArray(curr)
|
||||||
|
? [curr[0], curr[1]]
|
||||||
|
: [curr, {}];
|
||||||
|
return <Comp {...props}>{acc}</Comp>;
|
||||||
}, children)}
|
}, children)}
|
||||||
</>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://juliuskoronci.medium.com/avoid-a-long-list-of-react-providers-c45a269d80c1
|
||||||
Compose.propTypes = {
|
Compose.propTypes = {
|
||||||
components: PropTypes.array,
|
components: PropTypes.array,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
|
|||||||
@ -80,7 +80,9 @@ export default function ModeRoute({
|
|||||||
const {
|
const {
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
|
UserAuthenticationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode;
|
const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode;
|
||||||
|
|
||||||
if (dataSourceName === undefined) {
|
if (dataSourceName === undefined) {
|
||||||
@ -96,6 +98,12 @@ export default function ModeRoute({
|
|||||||
// Only handling one route per mode for now
|
// Only handling one route per mode for now
|
||||||
const route = mode.routes[0];
|
const route = mode.routes[0];
|
||||||
|
|
||||||
|
const layoutTemplateRouteData = route.layoutTemplate({ location });
|
||||||
|
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
|
||||||
|
layoutTemplateRouteData.id
|
||||||
|
);
|
||||||
|
const LayoutComponent = layoutTemplateModuleEntry.component;
|
||||||
|
|
||||||
// For each extension, look up their context modules
|
// For each extension, look up their context modules
|
||||||
// TODO: move to extension manager.
|
// TODO: move to extension manager.
|
||||||
let contextModules = [];
|
let contextModules = [];
|
||||||
|
|||||||
15
platform/viewer/src/routes/PrivateRoute.jsx
Normal file
15
platform/viewer/src/routes/PrivateRoute.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Route } from "react-router-dom";
|
||||||
|
import { useUserAuthentication } from "@ohif/ui";
|
||||||
|
|
||||||
|
export const PrivateRoute = ({ ...rest }) => {
|
||||||
|
const [{ user, enabled }, userAuthenticationService] = useUserAuthentication();
|
||||||
|
|
||||||
|
if (enabled && !user) {
|
||||||
|
return userAuthenticationService.handleUnauthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Route {...rest}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrivateRoute;
|
||||||
33
platform/viewer/src/routes/SignoutCallbackComponent.js
Normal file
33
platform/viewer/src/routes/SignoutCallbackComponent.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function SignoutCallbackComponent({ userManager }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onRedirectSuccess = (/* user */) => {
|
||||||
|
const { pathname, search = '' } = JSON.parse(
|
||||||
|
sessionStorage.getItem('ohif-redirect-to')
|
||||||
|
);
|
||||||
|
|
||||||
|
navigate(`${pathname}?${search}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRedirectError = error => {
|
||||||
|
throw new Error(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
userManager
|
||||||
|
.signoutRedirectCallback()
|
||||||
|
.then(user => onRedirectSuccess(user))
|
||||||
|
.catch(error => onRedirectError(error));
|
||||||
|
|
||||||
|
// todo: add i18n
|
||||||
|
return <div>Redirecting...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignoutCallbackComponent.propTypes = {
|
||||||
|
userManager: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignoutCallbackComponent;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import qs from 'query-string';
|
import qs from 'query-string';
|
||||||
import isEqual from 'lodash.isequal';
|
import isEqual from 'lodash.isequal';
|
||||||
@ -38,7 +38,6 @@ const seriesInStudiesMap = new Map();
|
|||||||
* - debounce `setFilterValues` (150ms?)
|
* - debounce `setFilterValues` (150ms?)
|
||||||
*/
|
*/
|
||||||
function WorkList({
|
function WorkList({
|
||||||
history,
|
|
||||||
data: studies,
|
data: studies,
|
||||||
dataTotal: studiesTotal,
|
dataTotal: studiesTotal,
|
||||||
isLoadingData,
|
isLoadingData,
|
||||||
@ -52,6 +51,7 @@ function WorkList({
|
|||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
// ~ Filters
|
// ~ Filters
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
|
const navigate = useNavigate();
|
||||||
const STUDIES_LIMIT = 101;
|
const STUDIES_LIMIT = 101;
|
||||||
const queryFilterValues = _getQueryFilterValues(query);
|
const queryFilterValues = _getQueryFilterValues(query);
|
||||||
const [filterValues, _setFilterValues] = useState({
|
const [filterValues, _setFilterValues] = useState({
|
||||||
@ -175,7 +175,7 @@ function WorkList({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
history.push({
|
navigate({
|
||||||
pathname: '/',
|
pathname: '/',
|
||||||
search: `?${qs.stringify(queryString, {
|
search: `?${qs.stringify(queryString, {
|
||||||
skipNull: true,
|
skipNull: true,
|
||||||
@ -441,9 +441,6 @@ function WorkList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
WorkList.propTypes = {
|
WorkList.propTypes = {
|
||||||
history: PropTypes.shape({
|
|
||||||
push: PropTypes.func,
|
|
||||||
}).isRequired,
|
|
||||||
data: PropTypes.array.isRequired,
|
data: PropTypes.array.isRequired,
|
||||||
dataSource: PropTypes.shape({
|
dataSource: PropTypes.shape({
|
||||||
query: PropTypes.object.isRequired,
|
query: PropTypes.object.isRequired,
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export default function buildModeRoutes({
|
|||||||
dataSources,
|
dataSources,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
hotkeysManager
|
hotkeysManager,
|
||||||
}) {
|
}) {
|
||||||
const routes = [];
|
const routes = [];
|
||||||
|
|
||||||
@ -51,20 +51,19 @@ export default function buildModeRoutes({
|
|||||||
|
|
||||||
// TODO move up.
|
// TODO move up.
|
||||||
const component = ({ location }) => (
|
const component = ({ location }) => (
|
||||||
<ModeRoute
|
<ModeRoute
|
||||||
location={location}
|
location={location}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
dataSourceName={dataSourceName}
|
dataSourceName={dataSourceName}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
hotkeysManager={hotkeysManager}
|
hotkeysManager={hotkeysManager}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
routes.push({
|
routes.push({
|
||||||
path,
|
path,
|
||||||
component,
|
component,
|
||||||
exact: true,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -74,21 +73,21 @@ export default function buildModeRoutes({
|
|||||||
const path = `/${mode.id}`;
|
const path = `/${mode.id}`;
|
||||||
|
|
||||||
// TODO move up.
|
// TODO move up.
|
||||||
const component = ({ location }) => (
|
const children = ({ location }) => (
|
||||||
<ModeRoute
|
<ModeRoute
|
||||||
location={location}
|
location={location}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
dataSourceName={defaultDataSourceName}
|
dataSourceName={defaultDataSourceName}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
hotkeysManager={hotkeysManager}
|
hotkeysManager={hotkeysManager}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
routes.push({
|
routes.push({
|
||||||
path,
|
path,
|
||||||
component,
|
children,
|
||||||
exact: true,
|
private: true, // todo: all mode routes are private for now
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Switch, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
|
import { ErrorBoundary } from '@ohif/ui';
|
||||||
|
|
||||||
// Route Components
|
// Route Components
|
||||||
import DataSourceWrapper from './DataSourceWrapper';
|
import DataSourceWrapper from './DataSourceWrapper';
|
||||||
import WorkList from './WorkList';
|
import WorkList from './WorkList';
|
||||||
import Local from './Local';
|
import Local from './Local';
|
||||||
import NotFound from './NotFound';
|
import NotFound from './NotFound';
|
||||||
import buildModeRoutes from './buildModeRoutes';
|
import buildModeRoutes from './buildModeRoutes';
|
||||||
import { ErrorBoundary } from '@ohif/ui';
|
import PrivateRoute from './PrivateRoute';
|
||||||
|
|
||||||
// TODO: Make these configurable
|
// TODO: Make these configurable
|
||||||
// TODO: Include "routes" debug route if dev build
|
// TODO: Include "routes" debug route if dev build
|
||||||
@ -14,14 +16,13 @@ const bakedInRoutes = [
|
|||||||
// WORK LIST
|
// WORK LIST
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
exact: true,
|
children: DataSourceWrapper,
|
||||||
component: DataSourceWrapper,
|
private: true,
|
||||||
props: { children: WorkList },
|
props: { children: WorkList },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/local',
|
path: '/local',
|
||||||
exact: true,
|
children: Local,
|
||||||
component: Local,
|
|
||||||
},
|
},
|
||||||
// NOT FOUND (404)
|
// NOT FOUND (404)
|
||||||
{ component: NotFound },
|
{ component: NotFound },
|
||||||
@ -33,6 +34,7 @@ const createRoutes = ({
|
|||||||
extensionManager,
|
extensionManager,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
hotkeysManager,
|
hotkeysManager,
|
||||||
|
routerBasename,
|
||||||
}) => {
|
}) => {
|
||||||
const routes =
|
const routes =
|
||||||
buildModeRoutes({
|
buildModeRoutes({
|
||||||
@ -45,31 +47,44 @@ const createRoutes = ({
|
|||||||
|
|
||||||
const allRoutes = [...routes, ...bakedInRoutes];
|
const allRoutes = [...routes, ...bakedInRoutes];
|
||||||
|
|
||||||
|
function RouteWithErrorBoundary({ route, ...rest }) {
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return (
|
||||||
|
<ErrorBoundary context={`Route ${route.path}`} fallbackRoute="/">
|
||||||
|
<route.children
|
||||||
|
{...rest}
|
||||||
|
{...route.props}
|
||||||
|
route={route}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
hotkeysManager={hotkeysManager}
|
||||||
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { UserAuthenticationService } = servicesManager.services;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Routes basename={routerBasename}>
|
||||||
{allRoutes.map((route, i) => {
|
{allRoutes.map((route, i) => {
|
||||||
return (
|
return route.private === true ? (
|
||||||
|
<PrivateRoute
|
||||||
|
key={i}
|
||||||
|
path={route.path}
|
||||||
|
handleUnauthenticated={
|
||||||
|
UserAuthenticationService.handleUnauthenticated
|
||||||
|
}
|
||||||
|
element={<RouteWithErrorBoundary route={route} />}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Route
|
<Route
|
||||||
key={i}
|
key={i}
|
||||||
path={route.path}
|
path={route.path}
|
||||||
exact={route.exact}
|
element={<RouteWithErrorBoundary route={route} />}
|
||||||
strict={route.strict}
|
|
||||||
render={props => (
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
||||||
<ErrorBoundary context={`Route ${route.path}`} fallbackRoute="/">
|
|
||||||
<route.component
|
|
||||||
{...props}
|
|
||||||
{...route.props}
|
|
||||||
route={route}
|
|
||||||
servicesManager={servicesManager}
|
|
||||||
hotkeysManager={hotkeysManager}
|
|
||||||
/>
|
|
||||||
</ErrorBoundary>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Switch>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
180
platform/viewer/src/utils/OpenIdConnectRoutes.jsx
Normal file
180
platform/viewer/src/utils/OpenIdConnectRoutes.jsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Route, Routes, useLocation, useNavigate } from 'react-router';
|
||||||
|
import CallbackPage from '../routes/CallbackPage';
|
||||||
|
import SignoutCallbackComponent from '../routes/SignoutCallbackComponent';
|
||||||
|
import getUserManagerForOpenIdConnectClient from './getUserManagerForOpenIdConnectClient.js';
|
||||||
|
|
||||||
|
function _isAbsoluteUrl(url) {
|
||||||
|
return url.includes('http://') || url.includes('https://');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeAbsoluteIfNecessary(url, base_url) {
|
||||||
|
if (_isAbsoluteUrl(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Make sure base_url and url are not duplicating slashes.
|
||||||
|
*/
|
||||||
|
if (base_url[base_url.length - 1] === '/') {
|
||||||
|
base_url = base_url.slice(0, base_url.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return base_url + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initUserManager = (oidc, routerBasename) => {
|
||||||
|
if (!oidc || !oidc.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstOpenIdClient = oidc[0];
|
||||||
|
const { protocol, host } = window.location;
|
||||||
|
const baseUri = `${protocol}//${host}${routerBasename}`;
|
||||||
|
|
||||||
|
const redirect_uri = firstOpenIdClient.redirect_uri || '/callback';
|
||||||
|
const silent_redirect_uri =
|
||||||
|
firstOpenIdClient.silent_redirect_uri || '/silent-refresh.html';
|
||||||
|
const post_logout_redirect_uri =
|
||||||
|
firstOpenIdClient.post_logout_redirect_uri || '/';
|
||||||
|
|
||||||
|
const openIdConnectConfiguration = Object.assign({}, firstOpenIdClient, {
|
||||||
|
redirect_uri: _makeAbsoluteIfNecessary(redirect_uri, baseUri),
|
||||||
|
silent_redirect_uri: _makeAbsoluteIfNecessary(
|
||||||
|
silent_redirect_uri,
|
||||||
|
baseUri
|
||||||
|
),
|
||||||
|
post_logout_redirect_uri: _makeAbsoluteIfNecessary(
|
||||||
|
post_logout_redirect_uri,
|
||||||
|
baseUri
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return getUserManagerForOpenIdConnectClient(openIdConnectConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoginComponent(userManager) {
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
const iss = queryParams.get('iss');
|
||||||
|
const loginHint = queryParams.get('login_hint');
|
||||||
|
const targetLinkUri = queryParams.get('target_link_uri');
|
||||||
|
if (iss !== oidcAuthority) {
|
||||||
|
console.error(
|
||||||
|
'iss of /login does not match the oidc authority'
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
userManager.removeUser().then(() => {
|
||||||
|
if (targetLinkUri !== null) {
|
||||||
|
const ohifRedirectTo = {
|
||||||
|
pathname: new URL(targetLinkUri).pathname,
|
||||||
|
};
|
||||||
|
sessionStorage.setItem(
|
||||||
|
'ohif-redirect-to',
|
||||||
|
JSON.stringify(ohifRedirectTo)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const ohifRedirectTo = {
|
||||||
|
pathname: '/',
|
||||||
|
};
|
||||||
|
sessionStorage.setItem(
|
||||||
|
'ohif-redirect-to',
|
||||||
|
JSON.stringify(ohifRedirectTo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginHint !== null) {
|
||||||
|
userManager.signinRedirect({ login_hint: loginHint });
|
||||||
|
} else {
|
||||||
|
userManager.signinRedirect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OpenIdConnectRoutes({
|
||||||
|
oidc,
|
||||||
|
routerBasename,
|
||||||
|
UserAuthenticationService
|
||||||
|
}) {
|
||||||
|
const userManager = initUserManager(oidc, routerBasename);
|
||||||
|
const getAuthorizationHeader = (user) => {
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${user.access_token}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUnauthenticated = () => {
|
||||||
|
userManager.signinRedirect()
|
||||||
|
|
||||||
|
// return null because this is used in a react component
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
UserAuthenticationService.setServiceImplementation({
|
||||||
|
getAuthorizationHeader,
|
||||||
|
handleUnauthenticated
|
||||||
|
});
|
||||||
|
|
||||||
|
const oidcAuthority = oidc[0].authority;
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const { pathname, search } = location;
|
||||||
|
|
||||||
|
const redirect_uri = new URL(userManager.settings._redirect_uri).pathname//.replace(routerBasename,'')
|
||||||
|
const silent_refresh_uri = new URL(userManager.settings._silent_redirect_uri).pathname//.replace(routerBasename,'')
|
||||||
|
const post_logout_redirect_uri = new URL(userManager.settings._post_logout_redirect_uri).pathname//.replace(routerBasename,'');
|
||||||
|
|
||||||
|
// const pathnameRelative = pathname.replace(routerBasename,'');
|
||||||
|
|
||||||
|
if (pathname !== redirect_uri) {
|
||||||
|
sessionStorage.setItem(
|
||||||
|
'ohif-redirect-to',
|
||||||
|
JSON.stringify({ pathname, search })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes basename={routerBasename}>
|
||||||
|
<Route
|
||||||
|
path={silent_refresh_uri}
|
||||||
|
onEnter={window.location.reload}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={post_logout_redirect_uri}
|
||||||
|
element={
|
||||||
|
<SignoutCallbackComponent
|
||||||
|
userManager={userManager}
|
||||||
|
successCallback={() => console.log('Signout successful')}
|
||||||
|
errorCallback={error => {
|
||||||
|
console.warn(error);
|
||||||
|
console.warn('Signout failed');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={redirect_uri}
|
||||||
|
element={<CallbackPage userManager={userManager} onRedirectSuccess={(user) => {
|
||||||
|
const { pathname, search = '' } = JSON.parse(
|
||||||
|
sessionStorage.getItem('ohif-redirect-to')
|
||||||
|
);
|
||||||
|
|
||||||
|
UserAuthenticationService.setUser(user);
|
||||||
|
|
||||||
|
navigate(`${pathname}?${search}`);
|
||||||
|
}}/>}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/login"
|
||||||
|
element={<LoginComponent userManager={userManager} oidcAuthority={oidcAuthority}/>}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OpenIdConnectRoutes;
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { UserManager } from 'oidc-client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a userManager from oidcSettings
|
||||||
|
* LINK: https://github.com/IdentityModel/oidc-client-js/wiki#configuration
|
||||||
|
*
|
||||||
|
* @param {Object} oidcSettings
|
||||||
|
* @param {string} oidcSettings.authServerUrl,
|
||||||
|
* @param {string} oidcSettings.clientId,
|
||||||
|
* @param {string} oidcSettings.authRedirectUri,
|
||||||
|
* @param {string} oidcSettings.postLogoutRedirectUri,
|
||||||
|
* @param {string} oidcSettings.responseType,
|
||||||
|
* @param {string} oidcSettings.extraQueryParams,
|
||||||
|
*/
|
||||||
|
export default function getUserManagerForOpenIdConnectClient(oidcSettings) {
|
||||||
|
if (!oidcSettings) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
...oidcSettings,
|
||||||
|
automaticSilentRenew: true,
|
||||||
|
revokeAccessTokenOnSignout: true,
|
||||||
|
filterProtocolClaims: true,
|
||||||
|
loadUserInfo: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userManager = new UserManager(settings);
|
||||||
|
|
||||||
|
return userManager;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user