Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Closes #157, Closes #162] Update client routing implementation #174

Merged
merged 9 commits into from
Dec 20, 2024
Merged
191 changes: 33 additions & 158 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,187 +1,62 @@
import React, { useContext, useEffect } from 'react';
import { Outlet, Routes, Route, useLocation, useNavigate } from 'react-router';
import { Loader } from '@mantine/core';
import { useContext, useEffect, ReactElement } from 'react';
import { useLocation, useNavigate, Outlet } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import PropTypes from 'prop-types';

import { Layout } from './stories/Layout/Layout';
import Home from './pages/home';
import Login from './pages/auth/login/login';
import Register from './pages/auth/register/register';
import Dashboard from './pages/dashboard/Dashboard';
import AdminPatientsGenerate from './pages/admin/patients/AdminPatientsGenerate';
import NotFound from './pages/notFound/NotFound';
import { AdminUsers } from './pages/admin/users/AdminUsers';
import { Center, Loader } from '@mantine/core';
import { Notifications } from '@mantine/notifications';

import Context from './Context';
import AdminPendingUsers from './pages/admin/pending-users/AdminPendingUsers';
import PasswordForgot from './pages/auth/password-forgot/passwordForgot';
import PasswordReset from './pages/auth/password-reset/passwordReset';
import AuthLayout from './stories/AuthLayout/AuthLayout';
import Verify from './pages/verify/verify';
import PatientRegistration from './pages/patients/register/PatientRegistration';
import PatientDetails from './pages/patients/patient-details/PatientDetails';
import Patients from './pages/patients/Patients';

const RedirectProps = {
isLoading: PropTypes.bool.isRequired,
isLoggedIn: PropTypes.bool.isRequired,
isLoggedInRequired: PropTypes.bool,
};
import { useAuthorization } from './hooks/useAuthorization';

/**
* Redirects browser based on props
* @param {PropTypes.InferProps<typeof RedirectProps>} props
* @returns {React.ReactElement}
* Top-level application component. *
* @param {PropTypes.func} handleRedirects
* @returns {ReactElement}
*/
function Redirect({ isLoading, isLoggedIn, isLoggedInRequired }) {
function App({ handleRedirects }) {
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
if (!isLoading) {
if (isLoggedInRequired && !isLoggedIn) {
let redirectTo = `${location.pathname}`;
if (location.search) {
redirectTo = `${redirectTo}?${location.search}`;
}
navigate('/login', { state: { redirectTo } });
} else if (!isLoggedInRequired && isLoggedIn) {
navigate('/dashboard');
}
}
}, [isLoading, isLoggedIn, isLoggedInRequired, location, navigate]);
if (isLoading) {
return <Loader />;
}
return <Outlet />;
}

Redirect.propTypes = RedirectProps;

const ProtectedRouteProps = {
role: PropTypes.string.isRequired,
restrictedRoles: PropTypes.arrayOf(PropTypes.string).isRequired,
destination: PropTypes.string,
message: PropTypes.string,
children: PropTypes.element.isRequired,
};

/**
* Protect route elements that don't allow for FIRST_RESPONDER role
* @param {PropTypes.InferProps<typeof ProtectedRouteProps>} props
* @returns {React.ReactElement}
*/
function ProtectedRoute({
restrictedRoles,
role,
destination = 'notFound',
message,
children,
}) {
const navigate = useNavigate();
useEffect(() => {
if (restrictedRoles.includes(role)) {
if (destination === 'forbidden') {
navigate('/forbidden', {
replace: true,
});
} else {
navigate('/not-found', {
replace: true,
state: { message },
});
}
}
}, [restrictedRoles, role, navigate, destination, message]);

return restrictedRoles.includes(role) ? <Loader /> : children;
}

ProtectedRoute.propTypes = ProtectedRouteProps;

/**
* Top-level application component. *
* @returns {React.ReactElement}
*/
function App() {
const { user, setUser } = useContext(Context);
const { handleLogout } = useAuthorization();

const { isLoading } = useQuery({
queryKey: ['user'],
queryFn: () => {
return fetch('/api/v1/users/me', { credentials: 'include' })
.then((response) => response.json())
.then((response) => (response.ok ? response.json() : null))
.then((newUser) => {
setUser(newUser);
return newUser;
});
},
});
const isLoggedIn = !isLoading && !!user?.id;

return (
<>
<Routes>
<Route
element={
<Redirect
isLoading={isLoading}
isLoggedIn={isLoggedIn}
isLoggedInRequired={true}
/>
}
>
<Route
path="/admin/patients/generate"
element={<AdminPatientsGenerate />}
/>
<Route element={<Layout />}>
<Route path="/patients" element={<Patients />} />
<Route path="/patients/:patientId" element={<PatientDetails />} />
<Route
path="/patients/register/:patientId"
element={
user ? (
<ProtectedRoute
role={user?.role}
restrictedRoles={['FIRST_RESPONDER']}
message={'Patient does not exist.'}
>
<PatientRegistration />
</ProtectedRoute>
) : (
<Loader />
)
}
/>
useEffect(() => {
try {
handleRedirects(user, location, (to, options) =>
navigate(to, { ...options, replace: true }),
);
} catch {
handleLogout();
}
}, [handleRedirects, handleLogout, user, location, navigate]);

<Route path="/admin/users" element={<AdminUsers />} />
<Route
path="/admin/pending-users"
element={<AdminPendingUsers />}
/>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="*" element={<NotFound />} />
</Route>
</Route>
<Route
element={<Redirect isLoading={isLoading} isLoggedIn={isLoggedIn} />}
>
<Route path="/" element={<Home />} />
<Route element={<AuthLayout />}>
<Route path="/register" element={<Register />} />
<Route path="/register/:inviteId" element={<Register />} />
<Route path="/login" element={<Login />} />
<Route path="/password/forgot" element={<PasswordForgot />} />
<Route
path="/password/:passwordResetToken"
element={<PasswordReset />}
/>
<Route path="verify/:emailVerificationToken" element={<Verify />} />
</Route>
</Route>
</Routes>
return isLoading ? (
<Center w="100vw" h="100vh">
<Loader />
</Center>
) : (
<>
<Outlet />
<Notifications position="bottom-right" />
</>
);
}

App.propTypes = {
handleRedirects: PropTypes.func.isRequired,
};

export default App;
15 changes: 0 additions & 15 deletions client/src/components/NavBar.jsx

This file was deleted.

18 changes: 15 additions & 3 deletions client/src/components/Sidebar/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,19 @@ export function Sidebar({ toggleSidebar }) {
*/
async function onLogout(event) {
event.preventDefault();
handleLogout();
await handleLogout();
}

return (
<>
<Stack justify="space-between" px="md" py="xl" w="100%" h="100%">
<Stack
className={classes.navbar}
justify="space-between"
px="md"
py="xl"
w="100%"
h="100%"
>
<Box>
<Group align="center" gap="sm" mb="lg">
<img
Expand Down Expand Up @@ -122,7 +129,12 @@ export function Sidebar({ toggleSidebar }) {
</Box>
))}
</Box>
<Group className={classes.footer} justify="space-between" align="top">
<Group
className={classes.footer}
justify="space-between"
align="top"
wrap="nowrap"
>
<Box fz="sm">
{user && (
<>
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/Sidebar/Sidebar.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.navbar {
border-right: 1px solid var(--mantine-color-gray-3);
min-width: 19.5rem;
}

.navbar__icon {
Expand All @@ -10,6 +10,7 @@
.footer {
border-top: 1px solid var(--mantine-color-gray-3);
padding: 1.25rem 0 0;
min-height: 4rem;
}

.footer__logout {
Expand Down
14 changes: 3 additions & 11 deletions client/src/hooks/useAuthorization.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useContext, useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router';

import Context from '../Context';

Expand All @@ -19,7 +18,6 @@ import Context from '../Context';
export function useAuthorization() {
const { user, setUser } = useContext(Context);
const [error, setError] = useState(null);
const navigate = useNavigate();

const loginMutation = useMutation({
mutationFn: async (credentials) => {
Expand All @@ -37,10 +35,9 @@ export function useAuthorization() {
return response;
});
},
onSuccess: async (data, { redirectTo }) => {
onSuccess: async (data) => {
const result = await data.json();
setUser(result);
navigate(redirectTo ?? '/');
},
onError: async (error) => {
const errorBody = await error.json();
Expand All @@ -54,17 +51,12 @@ export function useAuthorization() {
},
onSuccess: () => {
setUser(null);
navigate('/');
},
});

const handleLogin = async (credentials) => {
loginMutation.mutate(credentials);
};
const handleLogin = (credentials) => loginMutation.mutateAsync(credentials);

const handleLogout = async () => {
logoutMutation.mutate();
};
const handleLogout = () => logoutMutation.mutateAsync();

return {
user,
Expand Down
26 changes: 12 additions & 14 deletions client/src/main.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createBrowserRouter, RouterProvider } from 'react-router';
import { MantineProvider } from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import routes from './routes';
import theme from './theme';

import { ContextProvider } from './Context.jsx';

import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import '@mantine/dates/styles.css';

import App from './App.jsx';
import { theme } from './theme';

import { ContextProvider } from './Context.jsx';

const queryClient = new QueryClient({});

const router = createBrowserRouter(routes);

ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ContextProvider>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<MantineProvider theme={theme}>
<Notifications position="bottom-right" />
<App />
</MantineProvider>
</BrowserRouter>
<MantineProvider theme={theme}>
<RouterProvider router={router}></RouterProvider>
</MantineProvider>
</QueryClientProvider>
</ContextProvider>
</React.StrictMode>,
Expand Down
Loading
Loading