Skip to content

Commit

Permalink
Merge branch 'dev' into SIMSBIOHUB-73
Browse files Browse the repository at this point in the history
  • Loading branch information
al-rosenthal authored Jun 15, 2023
2 parents 591c19a + f11605d commit 735a389
Show file tree
Hide file tree
Showing 19 changed files with 329 additions and 157 deletions.
11 changes: 11 additions & 0 deletions api/src/utils/string-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,14 @@ export function safeTrim<T>(value: T): T {

return value;
}

/**
* Generates a login URL the includes an optional redirect URL.
*
* @param {string} host The host of the application
* @param {[string]} redirectTo The URL that the user will be redirected to upon logging in
* @returns The login URL
*/
export const makeLoginUrl = (host: string, redirectTo?: string) => {
return `${host}/login${redirectTo && `?redirect=${encodeURIComponent(redirectTo)}`}`;
};
124 changes: 73 additions & 51 deletions app/src/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,108 @@
import { SystemRoleGuard } from 'components/security/Guards';
import { AuthenticatedRouteGuard } from 'components/security/RouteGuards';
import { AuthenticatedRouteGuard, UnAuthenticatedRouteGuard } from 'components/security/RouteGuards';
import { SYSTEM_ROLE } from 'constants/roles';
import AccessDenied from 'features/403/AccessDenied';
import NotFoundPage from 'features/404/NotFoundPage';
import AdminUsersRouter from 'features/admin/AdminUsersRouter';
import AdminDashboardRouter from 'features/admin/dashboard/AdminDashboardRouter';
import DatasetsRouter from 'features/datasets/DatasetsRouter';
import HomeRouter from 'features/home/HomeRouter';
import LogOutPage from 'features/logout/LogOutPage';
import MapRouter from 'features/map/MapRouter';
import SearchRouter from 'features/search/SearchRouter';
import BaseLayout from 'layouts/BaseLayout';
import ContentLayout from 'layouts/ContentLayout';
import { Redirect, Switch, useLocation } from 'react-router-dom';
import AppRoute from 'utils/AppRoute';
import LoginPage from 'pages/authentication/LoginPage';
import LogOutPage from 'pages/authentication/LogOutPage';
import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
import RouteWithTitle from 'utils/RouteWithTitle';
import { getTitle } from 'utils/Utils';

const AppRouter: React.FC<React.PropsWithChildren> = () => {
const location = useLocation();

const getTitle = (page: string) => {
return `BioHub - ${page}`;
};

return (
<Switch>
<Redirect from="/:url*(/+)" to={{ ...location, pathname: location.pathname.slice(0, -1) }} />

<AppRoute exact path="/" title={getTitle('Home')} layout={BaseLayout}>
<HomeRouter />
</AppRoute>
<Route exact path="/">
<BaseLayout>
<HomeRouter />
</BaseLayout>
</Route>

<AppRoute path="/search" title={getTitle('Search')} layout={BaseLayout}>
<SearchRouter />
</AppRoute>
<Route path="/search">
<BaseLayout>
<SearchRouter />
</BaseLayout>
</Route>

<AppRoute path="/datasets" title={getTitle('Datasets')} layout={BaseLayout}>
<DatasetsRouter />
</AppRoute>
<Route path="/datasets">
<BaseLayout>
<DatasetsRouter />
</BaseLayout>
</Route>

<AppRoute path="/map" title={getTitle('Map')} layout={ContentLayout}>
<MapRouter />
</AppRoute>
<Route path="/map">
<ContentLayout>
<MapRouter />
</ContentLayout>
</Route>

<AppRoute path="/page-not-found" title={getTitle('Page Not Found')} layout={BaseLayout}>
<NotFoundPage />
</AppRoute>
<RouteWithTitle path="/page-not-found" title={getTitle('Page Not Found')}>
<BaseLayout>
<NotFoundPage />
</BaseLayout>
</RouteWithTitle>

<AppRoute path="/forbidden" title={getTitle('Forbidden')} layout={BaseLayout}>
<AccessDenied />
</AppRoute>
<RouteWithTitle path="/forbidden" title={getTitle('Forbidden')}>
<BaseLayout>
<AccessDenied />
</BaseLayout>
</RouteWithTitle>

<Redirect exact from="/admin" to="/admin/dashboard" />

<AppRoute exact path="/admin/dashboard" title={getTitle('Dashboard')} layout={BaseLayout}>
<AuthenticatedRouteGuard>
<SystemRoleGuard
validSystemRoles={[SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR]}
fallback={<Redirect to="/forbidden" />}>
<AdminDashboardRouter />
</SystemRoleGuard>
</AuthenticatedRouteGuard>
</AppRoute>
<Route exact path="/admin/dashboard">
<BaseLayout>
<AuthenticatedRouteGuard>
<SystemRoleGuard
validSystemRoles={[SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR]}
fallback={<Redirect to="/forbidden" />}>
<AdminDashboardRouter />
</SystemRoleGuard>
</AuthenticatedRouteGuard>
</BaseLayout>
</Route>

<Route path="/admin/users">
<BaseLayout>
<AuthenticatedRouteGuard>
<SystemRoleGuard
validSystemRoles={[SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR]}
fallback={<Redirect to="/forbidden" />}>
<AdminUsersRouter />
</SystemRoleGuard>
</AuthenticatedRouteGuard>
</BaseLayout>
</Route>

<AppRoute path="/admin/users" title={getTitle('Users')} layout={BaseLayout}>
<AuthenticatedRouteGuard>
<SystemRoleGuard
validSystemRoles={[SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR]}
fallback={<Redirect to="/forbidden" />}>
<AdminUsersRouter />
</SystemRoleGuard>
</AuthenticatedRouteGuard>
</AppRoute>
<RouteWithTitle path="/logout" title={getTitle('Logout')}>
<BaseLayout>
<AuthenticatedRouteGuard>
<LogOutPage />
</AuthenticatedRouteGuard>
</BaseLayout>
</RouteWithTitle>

<AppRoute path="/logout" title={getTitle('Logout')} layout={BaseLayout}>
<AuthenticatedRouteGuard>
<LogOutPage />
</AuthenticatedRouteGuard>
</AppRoute>
<Route path="/login">
<UnAuthenticatedRouteGuard>
<LoginPage />
</UnAuthenticatedRouteGuard>
</Route>

<AppRoute title="*" path="*">
<RouteWithTitle title="*" path="*">
<Redirect to="/page-not-found" />
</AppRoute>
</RouteWithTitle>
</Switch>
);
};
Expand Down
72 changes: 51 additions & 21 deletions app/src/components/security/RouteGuards.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import CircularProgress from '@mui/material/CircularProgress';
import { AuthStateContext } from 'contexts/authStateContext';
import qs from 'qs';
import React, { useContext } from 'react';
import useRedirect from 'hooks/useRedirect';
import React, { PropsWithChildren, useContext } from 'react';
import { Redirect, Route, RouteProps, useLocation } from 'react-router';

/**
Expand All @@ -16,46 +16,33 @@ import { Redirect, Route, RouteProps, useLocation } from 'react-router';
*/
export const AuthenticatedRouteGuard: React.FC<React.PropsWithChildren<RouteProps>> = ({ children, ...rest }) => {
return (
<CheckForAuthLoginParam>
<CheckForKeycloakAuthenticated>
<WaitForKeycloakToLoadUserInfo>
<CheckIfAuthenticatedUser>
<Route {...rest}>{children}</Route>
</CheckIfAuthenticatedUser>
</WaitForKeycloakToLoadUserInfo>
</CheckForAuthLoginParam>
</CheckForKeycloakAuthenticated>
);
};

/**
* Checks for query param `authLogin=true`. If set, force redirect the user to the keycloak login page.
*
* Redirects the user as appropriate, or renders the `children`.
*
* @param {*} { children }
* @return {*}
*/
const CheckForAuthLoginParam: React.FC<React.PropsWithChildren> = ({ children }) => {
const CheckForKeycloakAuthenticated = (props: PropsWithChildren<Record<never, unknown>>) => {
const { keycloakWrapper } = useContext(AuthStateContext);

const location = useLocation();

if (!keycloakWrapper?.keycloak.authenticated) {
const urlParams = qs.parse(location.search, { ignoreQueryPrefix: true });
const authLoginUrlParam = urlParams.authLogin;
// check for urlParam to force login
if (authLoginUrlParam) {
// remove authLogin url param from url to stop possible loop redirect
const redirectUrlParams = qs.stringify(urlParams, { filter: (prefix) => prefix !== 'authLogin' });
const redirectUri = `${window.location.origin}${location.pathname}?${redirectUrlParams}`;

// trigger login
keycloakWrapper?.keycloak.login({ redirectUri: redirectUri });
}

return <Redirect to="/" />;
// Trigger login, then redirect to the desired route
return <Redirect to={`/login?redirect=${encodeURIComponent(location.pathname)}`} />;
}

return <>{children}</>;
return <>{props.children}</>;
};

/**
Expand Down Expand Up @@ -100,3 +87,46 @@ const CheckIfAuthenticatedUser: React.FC<React.PropsWithChildren> = ({ children

return <>{children}</>;
};

/**
* Route guard that requires the user to not be authenticated.
*
* @param {*} { children, ...rest }
* @return {*}
*/
export const UnAuthenticatedRouteGuard = (props: RouteProps) => {
const { children, ...rest } = props;

return (
<CheckIfNotAuthenticatedUser>
<Route {...rest}>{children}</Route>
</CheckIfNotAuthenticatedUser>
);
};

/**
* Checks if the user is not a registered user.
*
* Redirects the user as appropriate, or renders the `children`.
*
* @param {*} { children }
* @return {*}
*/
const CheckIfNotAuthenticatedUser = (props: PropsWithChildren<Record<never, unknown>>) => {
const { keycloakWrapper } = useContext(AuthStateContext);
const { redirect } = useRedirect('/');

if (keycloakWrapper?.keycloak.authenticated) {
/**
* If the user happens to be authenticated, rather than just redirecting them to `/`, we can
* check if the URL contains a redirect query param, and send them there instead (for
* example, links to `/login` generated by SIMS will typically include a redirect query param).
* If there is no redirect query param, they will be sent to `/` as a fallback.
*/
redirect();

return <></>;
}

return <>{props.children}</>;
};
13 changes: 7 additions & 6 deletions app/src/features/admin/AdminUsersRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Redirect, Switch } from 'react-router';
import AppRoute from 'utils/AppRoute';
import { Redirect, Route, Switch } from 'react-router';
import RouteWithTitle from 'utils/RouteWithTitle';
import { getTitle } from 'utils/Utils';
import ManageUsersPage from './users/ManageUsersPage';

/**
Expand All @@ -10,14 +11,14 @@ import ManageUsersPage from './users/ManageUsersPage';
const AdminUsersRouter: React.FC<React.PropsWithChildren> = () => {
return (
<Switch>
<AppRoute exact path="/admin/users">
<RouteWithTitle exact path="/admin/users" title={getTitle('Manage Users')}>
<ManageUsersPage />
</AppRoute>
</RouteWithTitle>

{/* Catch any unknown routes, and re-direct to the not found page */}
<AppRoute path="/admin/users/*">
<Route path="/admin/users/*">
<Redirect to="/page-not-found" />
</AppRoute>
</Route>
</Switch>
);
};
Expand Down
7 changes: 4 additions & 3 deletions app/src/features/admin/dashboard/AdminDashboardRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Switch } from 'react-router';
import AppRoute from 'utils/AppRoute';
import RouteWithTitle from 'utils/RouteWithTitle';
import { getTitle } from 'utils/Utils';
import DashboardPage from './DashboardPage';

/**
Expand All @@ -10,9 +11,9 @@ import DashboardPage from './DashboardPage';
const AdminDashboardRouter: React.FC<React.PropsWithChildren> = () => {
return (
<Switch>
<AppRoute exact path="/admin/dashboard">
<RouteWithTitle exact path="/admin/dashboard" title={getTitle('Dashboard')}>
<DashboardPage />
</AppRoute>
</RouteWithTitle>
</Switch>
);
};
Expand Down
13 changes: 7 additions & 6 deletions app/src/features/datasets/DatasetsRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Redirect, Switch } from 'react-router';
import AppRoute from 'utils/AppRoute';
import { Redirect, Route, Switch } from 'react-router';
import RouteWithTitle from 'utils/RouteWithTitle';
import { getTitle } from 'utils/Utils';
import DatasetPage from './DatasetPage';

/**
Expand All @@ -12,14 +13,14 @@ const DatasetsRouter: React.FC<React.PropsWithChildren> = () => {
<Switch>
<Redirect exact from="/datasets/:id" to="/datasets/:id/details" />

<AppRoute exact path="/datasets/:id/details">
<RouteWithTitle exact path="/datasets/:id/details" title={getTitle('Datasets')}>
<DatasetPage />
</AppRoute>
</RouteWithTitle>

{/* Catch any unknown routes, and re-direct to the not found page */}
<AppRoute path="/datasets/*">
<Route path="/datasets/*">
<Redirect to="/page-not-found" />
</AppRoute>
</Route>
</Switch>
);
};
Expand Down
7 changes: 4 additions & 3 deletions app/src/features/home/HomeRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Switch } from 'react-router';
import AppRoute from 'utils/AppRoute';
import RouteWithTitle from 'utils/RouteWithTitle';
import { getTitle } from 'utils/Utils';
import HomePage from './HomePage';

/**
Expand All @@ -10,9 +11,9 @@ import HomePage from './HomePage';
const HomeRouter: React.FC<React.PropsWithChildren> = () => {
return (
<Switch>
<AppRoute exact path="/">
<RouteWithTitle exact path="/" title={getTitle('Home')}>
<HomePage />
</AppRoute>
</RouteWithTitle>
</Switch>
);
};
Expand Down
Loading

0 comments on commit 735a389

Please sign in to comment.