@@ -92,19 +70,21 @@ function LloydGeorgeRecordDetails({
- {actionLinks.map((link) => (
- -
- {
- e.preventDefault();
- link.handler();
- }}
- >
- {link.label}
-
-
- ))}
+ {actionLinks.map((link) =>
+ role && !link.unauthorised?.includes(role) ? (
+ -
+ {
+ e.preventDefault();
+ setStage(link.stage);
+ }}
+ >
+ {link.label}
+
+
+ ) : null,
+ )}
diff --git a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx
index 9a6de02a1..8e38cb8b8 100644
--- a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx
+++ b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx
@@ -5,12 +5,14 @@ import LgRecordStage, { Props } from './LloydGeorgeRecordStage';
import { getFormattedDate } from '../../../helpers/utils/formatDate';
import { DOWNLOAD_STAGE } from '../../../types/generic/downloadStage';
import { useState } from 'react';
-import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage';
import formatFileSize from '../../../helpers/utils/formatFileSize';
import { act } from 'react-dom/test-utils';
+import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages';
const mockPdf = buildLgSearchResult();
const mockPatientDetails = buildPatientDetails();
+jest.mock('../../../helpers/hooks/useRole');
+
describe('LloydGeorgeRecordStage', () => {
beforeEach(() => {
process.env.REACT_APP_ENVIRONMENT = 'jest';
diff --git a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx
index 7b2416bca..a05ddfd4b 100644
--- a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx
+++ b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx
@@ -4,9 +4,9 @@ import { BackLink, Card, Details } from 'nhsuk-react-components';
import { getFormattedDate } from '../../../helpers/utils/formatDate';
import { DOWNLOAD_STAGE } from '../../../types/generic/downloadStage';
import PdfViewer from '../../generic/pdfViewer/PdfViewer';
-import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage';
import LloydGeorgeRecordDetails from '../lloydGeorgeRecordDetails/LloydGeorgeRecordDetails';
import { formatNhsNumber } from '../../../helpers/utils/formatNhsNumber';
+import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages';
export type Props = {
patientDetails: PatientDetails;
diff --git a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx
index f64db89e1..421387875 100644
--- a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx
+++ b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx
@@ -10,13 +10,8 @@ import getLloydGeorgeRecord from '../../helpers/requests/getLloydGeorgeRecord';
import LloydGeorgeRecordStage from '../../components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage';
import LloydGeorgeDownloadAllStage from '../../components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage';
import { DOCUMENT_TYPE } from '../../types/pages/UploadDocumentsPage/types';
+import { LG_RECORD_STAGE } from '../../types/blocks/lloydGeorgeStages';
-export enum LG_RECORD_STAGE {
- RECORD = 0,
- DOWNLOAD_ALL = 1,
- SEE_ALL = 2,
- DELETE_ALL = 3,
-}
function LloydGeorgeRecordPage() {
const [patientDetails] = usePatientDetailsContext();
const [downloadStage, setDownloadStage] = useState(DOWNLOAD_STAGE.INITIAL);
diff --git a/app/src/router/AppRouter.tsx b/app/src/router/AppRouter.tsx
new file mode 100644
index 000000000..200c28608
--- /dev/null
+++ b/app/src/router/AppRouter.tsx
@@ -0,0 +1,158 @@
+import React from 'react';
+import { BrowserRouter as Router, Routes as Switch, Route, Outlet } from 'react-router-dom';
+import Layout from '../components/layout/Layout';
+import { ROUTE_TYPE, route, routes } from '../types/generic/routes';
+import HomePage from '../pages/homePage/HomePage';
+import AuthCallbackPage from '../pages/authCallbackPage/AuthCallbackPage';
+import NotFoundPage from '../pages/notFoundPage/NotFoundPage';
+import AuthErrorPage from '../pages/authErrorPage/AuthErrorPage';
+import UnauthorisedPage from '../pages/unauthorisedPage/UnauthorisedPage';
+import LogoutPage from '../pages/logoutPage/LogoutPage';
+import PatientSearchPage from '../pages/patientSearchPage/PatientSearchPage';
+import PatientResultPage from '../pages/patientResultPage/PatientResultPage';
+import UploadDocumentsPage from '../pages/uploadDocumentsPage/UploadDocumentsPage';
+import DocumentSearchResultsPage from '../pages/documentSearchResultsPage/DocumentSearchResultsPage';
+import LloydGeorgeRecordPage from '../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage';
+import AuthGuard from './guards/authGuard/AuthGuard';
+import PatientGuard from './guards/patientGuard/PatientGuard';
+import { REPOSITORY_ROLE } from '../types/generic/authRole';
+import RoleGuard from './guards/roleGuard/RoleGuard';
+const {
+ HOME,
+ AUTH_CALLBACK,
+ NOT_FOUND,
+ UNAUTHORISED,
+ AUTH_ERROR,
+ LOGOUT,
+ DOWNLOAD_SEARCH,
+ DOWNLOAD_VERIFY,
+ DOWNLOAD_DOCUMENTS,
+ LLOYD_GEORGE,
+ UPLOAD_SEARCH,
+ UPLOAD_VERIFY,
+ UPLOAD_DOCUMENTS,
+} = routes;
+
+type Routes = {
+ [key in routes]: route;
+};
+
+export const routeMap: Routes = {
+ // Public routes
+ [HOME]: {
+ page:
,
+ type: ROUTE_TYPE.PUBLIC,
+ },
+ [AUTH_CALLBACK]: {
+ page:
,
+ type: ROUTE_TYPE.PUBLIC,
+ },
+ [NOT_FOUND]: {
+ page:
,
+ type: ROUTE_TYPE.PUBLIC,
+ },
+ [AUTH_ERROR]: {
+ page:
,
+ type: ROUTE_TYPE.PUBLIC,
+ },
+ [UNAUTHORISED]: {
+ page:
,
+ type: ROUTE_TYPE.PUBLIC,
+ },
+
+ // Auth guard routes
+ [LOGOUT]: {
+ page:
,
+ type: ROUTE_TYPE.PRIVATE,
+ },
+
+ // App guard routes
+ [DOWNLOAD_SEARCH]: {
+ page:
,
+ type: ROUTE_TYPE.PRIVATE,
+ unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL],
+ },
+ [UPLOAD_SEARCH]: {
+ page:
,
+ type: ROUTE_TYPE.PRIVATE,
+ unauthorized: [REPOSITORY_ROLE.PCSE],
+ },
+ [DOWNLOAD_VERIFY]: {
+ page:
,
+ type: ROUTE_TYPE.PATIENT,
+ unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL],
+ },
+ [UPLOAD_VERIFY]: {
+ page:
,
+ type: ROUTE_TYPE.PATIENT,
+ unauthorized: [REPOSITORY_ROLE.PCSE],
+ },
+ [UPLOAD_DOCUMENTS]: {
+ page:
,
+ type: ROUTE_TYPE.PATIENT,
+ unauthorized: [REPOSITORY_ROLE.PCSE],
+ },
+ [DOWNLOAD_DOCUMENTS]: {
+ page:
,
+ type: ROUTE_TYPE.PATIENT,
+ unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL],
+ },
+ [LLOYD_GEORGE]: {
+ page:
,
+ type: ROUTE_TYPE.PATIENT,
+ unauthorized: [REPOSITORY_ROLE.PCSE],
+ },
+};
+
+const createRoutesFromType = (routeType: ROUTE_TYPE) =>
+ Object.entries(routeMap).reduce(
+ (acc, [path, route]) =>
+ route.type === routeType
+ ? [...acc,
]
+ : acc,
+ [] as Array
,
+ );
+
+const AppRoutes = () => {
+ const publicRoutes = createRoutesFromType(ROUTE_TYPE.PUBLIC);
+ const privateRoutes = createRoutesFromType(ROUTE_TYPE.PRIVATE);
+ const patientRoutes = createRoutesFromType(ROUTE_TYPE.PATIENT);
+
+ return (
+
+ {publicRoutes}
+
+
+
+
+
+ }
+ >
+ {privateRoutes}
+
+
+
+ }
+ >
+ {patientRoutes}
+
+
+
+ );
+};
+
+const AppRouter = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default AppRouter;
diff --git a/app/src/components/blocks/authGuard/AuthGuard.test.tsx b/app/src/router/guards/authGuard/AuthGuard.test.tsx
similarity index 100%
rename from app/src/components/blocks/authGuard/AuthGuard.test.tsx
rename to app/src/router/guards/authGuard/AuthGuard.test.tsx
diff --git a/app/src/components/blocks/authGuard/AuthGuard.tsx b/app/src/router/guards/authGuard/AuthGuard.tsx
similarity index 100%
rename from app/src/components/blocks/authGuard/AuthGuard.tsx
rename to app/src/router/guards/authGuard/AuthGuard.tsx
diff --git a/app/src/components/blocks/patientGuard/PatientGuard.test.tsx b/app/src/router/guards/patientGuard/PatientGuard.test.tsx
similarity index 100%
rename from app/src/components/blocks/patientGuard/PatientGuard.test.tsx
rename to app/src/router/guards/patientGuard/PatientGuard.test.tsx
diff --git a/app/src/components/blocks/patientGuard/PatientGuard.tsx b/app/src/router/guards/patientGuard/PatientGuard.tsx
similarity index 100%
rename from app/src/components/blocks/patientGuard/PatientGuard.tsx
rename to app/src/router/guards/patientGuard/PatientGuard.tsx
diff --git a/app/src/router/guards/roleGuard/RoleGuard.test.tsx b/app/src/router/guards/roleGuard/RoleGuard.test.tsx
new file mode 100644
index 000000000..805f25904
--- /dev/null
+++ b/app/src/router/guards/roleGuard/RoleGuard.test.tsx
@@ -0,0 +1,59 @@
+import { render, waitFor } from '@testing-library/react';
+import * as ReactRouter from 'react-router';
+import { History, createMemoryHistory } from 'history';
+import { routes } from '../../../types/generic/routes';
+import RoleGuard from './RoleGuard';
+import useRole from '../../../helpers/hooks/useRole';
+import { REPOSITORY_ROLE } from '../../../types/generic/authRole';
+
+jest.mock('../../../helpers/hooks/useRole');
+const mockedUseRole = useRole as jest.Mock;
+
+const guardPage = routes.LLOYD_GEORGE;
+describe('RoleGuard', () => {
+ beforeEach(() => {
+ process.env.REACT_APP_ENVIRONMENT = 'jest';
+ });
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+ it('navigates user to unauthorised when role is not accepted', async () => {
+ const history = createMemoryHistory({
+ initialEntries: [guardPage],
+ initialIndex: 0,
+ });
+
+ mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE);
+ expect(history.location.pathname).toBe(guardPage);
+ renderAuthGuard(history);
+
+ await waitFor(async () => {
+ expect(history.location.pathname).toBe(routes.UNAUTHORISED);
+ });
+ });
+
+ it('navigates user to correct page when role is accepted', async () => {
+ const history = createMemoryHistory({
+ initialEntries: [guardPage],
+ initialIndex: 0,
+ });
+
+ mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN);
+ expect(history.location.pathname).toBe(guardPage);
+ renderAuthGuard(history);
+
+ await waitFor(async () => {
+ expect(history.location.pathname).toBe(guardPage);
+ });
+ });
+});
+
+const renderAuthGuard = (history: History) => {
+ return render(
+
+
+
+
+ ,
+ );
+};
diff --git a/app/src/router/guards/roleGuard/RoleGuard.tsx b/app/src/router/guards/roleGuard/RoleGuard.tsx
new file mode 100644
index 000000000..21dcaf20d
--- /dev/null
+++ b/app/src/router/guards/roleGuard/RoleGuard.tsx
@@ -0,0 +1,28 @@
+import { useEffect, type ReactNode } from 'react';
+import { useNavigate } from 'react-router';
+import { useLocation } from 'react-router-dom';
+import { routes } from '../../../types/generic/routes';
+import { routeMap } from '../../AppRouter';
+import useRole from '../../../helpers/hooks/useRole';
+
+type Props = {
+ children: ReactNode;
+};
+
+function RoleGuard({ children }: Props) {
+ const role = useRole();
+ const navigate = useNavigate();
+ const location = useLocation();
+ useEffect(() => {
+ const routeKey = location.pathname as keyof typeof routeMap;
+ const { unauthorized } = routeMap[routeKey];
+ const denyResource = Array.isArray(unauthorized) && role && unauthorized.includes(role);
+
+ if (denyResource) {
+ navigate(routes.UNAUTHORISED);
+ }
+ }, [role, location, navigate]);
+ return <>{children}>;
+}
+
+export default RoleGuard;
diff --git a/app/src/types/blocks/lloydGeorgeActions.ts b/app/src/types/blocks/lloydGeorgeActions.ts
new file mode 100644
index 000000000..8c7f34df5
--- /dev/null
+++ b/app/src/types/blocks/lloydGeorgeActions.ts
@@ -0,0 +1,29 @@
+import { REPOSITORY_ROLE } from '../generic/authRole';
+import { LG_RECORD_STAGE } from './lloydGeorgeStages';
+
+type PdfActionLink = {
+ label: string;
+ key: string;
+ stage: LG_RECORD_STAGE;
+ unauthorised?: Array;
+};
+
+export const actionLinks: Array = [
+ {
+ label: 'See all files',
+ key: 'see-all-files-link',
+ stage: LG_RECORD_STAGE.SEE_ALL,
+ },
+ {
+ label: 'Download all files',
+ key: 'download-all-files-link',
+ stage: LG_RECORD_STAGE.DOWNLOAD_ALL,
+ unauthorised: [REPOSITORY_ROLE.GP_CLINICAL],
+ },
+ {
+ label: 'Delete all files',
+ key: 'delete-all-files-link',
+ stage: LG_RECORD_STAGE.DELETE_ALL,
+ unauthorised: [REPOSITORY_ROLE.GP_CLINICAL],
+ },
+];
diff --git a/app/src/types/blocks/lloydGeorgeStages.ts b/app/src/types/blocks/lloydGeorgeStages.ts
new file mode 100644
index 000000000..662c9e86c
--- /dev/null
+++ b/app/src/types/blocks/lloydGeorgeStages.ts
@@ -0,0 +1,6 @@
+export enum LG_RECORD_STAGE {
+ RECORD = 0,
+ DOWNLOAD_ALL = 1,
+ SEE_ALL = 2,
+ DELETE_ALL = 3,
+}
diff --git a/app/src/types/generic/routes.ts b/app/src/types/generic/routes.ts
index ab05ac056..f174cb859 100644
--- a/app/src/types/generic/routes.ts
+++ b/app/src/types/generic/routes.ts
@@ -1,3 +1,5 @@
+import { REPOSITORY_ROLE } from './authRole';
+
export enum routes {
HOME = '/',
AUTH_CALLBACK = '/auth-callback',
@@ -10,7 +12,6 @@ export enum routes {
DOWNLOAD_SEARCH = '/search/patient',
DOWNLOAD_VERIFY = '/search/patient/result',
DOWNLOAD_DOCUMENTS = '/search/results',
- DELETE_DOCUMENTS = '/search/results/delete',
LLOYD_GEORGE = '/search/patient/lloyd-george-record',
@@ -18,3 +19,18 @@ export enum routes {
UPLOAD_VERIFY = '/search/upload/result',
UPLOAD_DOCUMENTS = '/upload/submit',
}
+
+export enum ROUTE_TYPE {
+ // No guard
+ PUBLIC = 0,
+ // Auth route guard
+ PRIVATE = 1,
+ // All route guards
+ PATIENT = 2,
+}
+
+export type route = {
+ page: JSX.Element;
+ type: ROUTE_TYPE;
+ unauthorized?: Array;
+};