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:
Erik Ziegler authored and GitHub committed 2021-06-23 20:41:27 +02:00
1 parent 5643f8f6d2
commit e7b32cc51e
50 files changed
+2746 -2109

No files matched your search

@@ -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 { MODULE_TYPES } from '@ohif/core';
//
import { useAppConfig } from '@state';
import { extensionManager } from '../App.jsx';
import { useParams, useLocation } from 'react-router';
/**
* 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
*/
function DataSourceWrapper(props) {
const [appConfig] = useAppConfig();
const { children: LayoutTemplate, history, ...rest } = props;
const { children: LayoutTemplate, ...rest } = props;
const params = useParams();
const location = useLocation();
// TODO: Fetch by type, name, etc?
const dataSourceModules = extensionManager.modules[MODULE_TYPES.DATA_SOURCE];
// TODO: Good usecase for flatmap?
@@ -30,13 +32,10 @@ function DataSourceWrapper(props) {
return acc.concat(mods);
}, []);
// Grabbing first for now. This isn't hydrated yet, but we should
// hydrate it somewhere based on config...
// ~ default.js
const firstAppConfigDataSource = appConfig.dataSources[0];
const dataSourceConfig = firstAppConfigDataSource.configuration;
const firstWebApiDataSource = webApiDataSources[0];
const dataSource = firstWebApiDataSource.createDataSource(dataSourceConfig);
// Grabbing first for now - should get active?
const name = webApiDataSources[0].name;
// TODO: Why does this return an array?
const dataSource = extensionManager.getDataSources(name)[0]
// Route props --> studies.mapParams
// mapParams --> studies.search
@@ -55,19 +54,18 @@ function DataSourceWrapper(props) {
useEffect(() => {
const queryFilterValues = _getQueryFilterValues(
history.location.search,
location.search,
STUDIES_LIMIT
);
// 204: no content
async function getData() {
setIsLoading(true);
const studies = await dataSource.query.studies.search(queryFilterValues);
setIsLoading(false);
setData({
studies,
studies: studies || [],
total: studies.length,
resultsPerPage: queryFilterValues.resultsPerPage,
pageNumber: queryFilterValues.pageNumber,
@@ -99,14 +97,13 @@ function DataSourceWrapper(props) {
console.warn(ex);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [history.location.search]);
}, [location, params]);
// queryFilterValues
// TODO: Better way to pass DataSource?
return (
<LayoutTemplate
{...rest}
history={history}
data={data.studies}
dataTotal={data.total}
dataSource={dataSource}
+8 -4
View File
@@ -10,14 +10,18 @@ export default function Compose(props) {
const { components = [], children } = props;
return (
<>
{components.reduceRight((acc, Comp) => {
return <Comp>{acc}</Comp>;
<React.Fragment>
{components.reduceRight((acc, curr) => {
const [Comp, props] = Array.isArray(curr)
? [curr[0], curr[1]]
: [curr, {}];
return <Comp {...props}>{acc}</Comp>;
}, children)}
</>
</React.Fragment>
);
}
// https://juliuskoronci.medium.com/avoid-a-long-list-of-react-providers-c45a269d80c1
Compose.propTypes = {
components: PropTypes.array,
children: PropTypes.node.isRequired,
+8
View File
@@ -80,7 +80,9 @@ export default function ModeRoute({
const {
DisplaySetService,
HangingProtocolService,
UserAuthenticationService,
} = servicesManager.services;
const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode;
if (dataSourceName === undefined) {
@@ -96,6 +98,12 @@ export default function ModeRoute({
// Only handling one route per mode for now
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
// TODO: move to extension manager.
let contextModules = [];
@@ -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;
@@ -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 classnames from 'classnames';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import moment from 'moment';
import qs from 'query-string';
import isEqual from 'lodash.isequal';
@@ -38,7 +38,6 @@ const seriesInStudiesMap = new Map();
* - debounce `setFilterValues` (150ms?)
*/
function WorkList({
history,
data: studies,
dataTotal: studiesTotal,
isLoadingData,
@@ -52,6 +51,7 @@ function WorkList({
const [appConfig] = useAppConfig();
// ~ Filters
const query = useQuery();
const navigate = useNavigate();
const STUDIES_LIMIT = 101;
const queryFilterValues = _getQueryFilterValues(query);
const [filterValues, _setFilterValues] = useState({
@@ -175,7 +175,7 @@ function WorkList({
}
});
history.push({
navigate({
pathname: '/',
search: `?${qs.stringify(queryString, {
skipNull: true,
@@ -441,9 +441,6 @@ function WorkList({
}
WorkList.propTypes = {
history: PropTypes.shape({
push: PropTypes.func,
}).isRequired,
data: PropTypes.array.isRequired,
dataSource: PropTypes.shape({
query: PropTypes.object.isRequired,
+20 -21
View File
@@ -26,7 +26,7 @@ export default function buildModeRoutes({
dataSources,
extensionManager,
servicesManager,
hotkeysManager
hotkeysManager,
}) {
const routes = [];
@@ -51,20 +51,19 @@ export default function buildModeRoutes({
// TODO move up.
const component = ({ location }) => (
<ModeRoute
location={location}
mode={mode}
dataSourceName={dataSourceName}
extensionManager={extensionManager}
servicesManager={servicesManager}
hotkeysManager={hotkeysManager}
/>
<ModeRoute
location={location}
mode={mode}
dataSourceName={dataSourceName}
extensionManager={extensionManager}
servicesManager={servicesManager}
hotkeysManager={hotkeysManager}
/>
);
routes.push({
path,
component,
exact: true,
});
});
@@ -74,21 +73,21 @@ export default function buildModeRoutes({
const path = `/${mode.id}`;
// TODO move up.
const component = ({ location }) => (
<ModeRoute
location={location}
mode={mode}
dataSourceName={defaultDataSourceName}
extensionManager={extensionManager}
servicesManager={servicesManager}
hotkeysManager={hotkeysManager}
/>
const children = ({ location }) => (
<ModeRoute
location={location}
mode={mode}
dataSourceName={defaultDataSourceName}
extensionManager={extensionManager}
servicesManager={servicesManager}
hotkeysManager={hotkeysManager}
/>
);
routes.push({
path,
component,
exact: true,
children,
private: true, // todo: all mode routes are private for now
});
});
+38 -23
View File
@@ -1,12 +1,14 @@
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
import DataSourceWrapper from './DataSourceWrapper';
import WorkList from './WorkList';
import Local from './Local';
import NotFound from './NotFound';
import buildModeRoutes from './buildModeRoutes';
import { ErrorBoundary } from '@ohif/ui';
import PrivateRoute from './PrivateRoute';
// TODO: Make these configurable
// TODO: Include "routes" debug route if dev build
@@ -14,14 +16,13 @@ const bakedInRoutes = [
// WORK LIST
{
path: '/',
exact: true,
component: DataSourceWrapper,
children: DataSourceWrapper,
private: true,
props: { children: WorkList },
},
{
path: '/local',
exact: true,
component: Local,
children: Local,
},
// NOT FOUND (404)
{ component: NotFound },
@@ -33,6 +34,7 @@ const createRoutes = ({
extensionManager,
servicesManager,
hotkeysManager,
routerBasename,
}) => {
const routes =
buildModeRoutes({
@@ -45,31 +47,44 @@ const createRoutes = ({
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 (
<Switch>
<Routes basename={routerBasename}>
{allRoutes.map((route, i) => {
return (
return route.private === true ? (
<PrivateRoute
key={i}
path={route.path}
handleUnauthenticated={
UserAuthenticationService.handleUnauthenticated
}
element={<RouteWithErrorBoundary route={route} />}
/>
) : (
<Route
key={i}
path={route.path}
exact={route.exact}
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>
)}
element={<RouteWithErrorBoundary route={route} />}
/>
);
})}
</Switch>
</Routes>
);
};