Skip to content

Commit

Permalink
feat(frontend, auth): authentication (data entry) design (#11)
Browse files Browse the repository at this point in the history
* Scaffold online login page

* Add auth0 login

* Use shared button

* Add offline login page

* Finish offline login page

* Move use network hook

* Add a modal for reconnection

* Make location select required

* Add protected route

* Check for internet connection

* Make online login work

* Remove console logs

* Fix refresh removing authentication

* Add function to open reconnected modal

* Make protected route work when offline authenticated

* Add tests

* Remove location select from login page

* Add log out button

* Balance login page

* 💅 make pretty
  • Loading branch information
yuliapechorina authored Sep 14, 2022
1 parent e6aeadb commit aea9eba
Show file tree
Hide file tree
Showing 29 changed files with 1,036 additions and 42 deletions.
1 change: 1 addition & 0 deletions apps/frontend/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default {
'^@root(.*)$': '<rootDir>/src$1',
'^@shared(.*)$': '<rootDir>/src/app/shared$1',
'^@patients(.*)$': '<rootDir>/src/app/patients$1',
'^@login(.*)$': '<rootDir>/src/app/login$1',
},
setupFiles: ['<rootDir>/setupTests.ts'],
};
61 changes: 39 additions & 22 deletions apps/frontend/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,58 @@ import {
} from './shared/modals/search/SearchModal';
import NavbarLink from './shared/navbar/link/navbar-link';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import {
ReconnectedLoginModal,
ReconnectedLoginModalRef,
} from './login/reconnected-login-modal';
import { UserAvatar } from './shared/user-avatar';

export function App() {
const navigate = useNavigate();
const location = useLocation();
const searchModal = useRef<SearchModalRef>(null);
const reconnectedLoginModal = useRef<ReconnectedLoginModalRef>(null);

// TODO: Call this when the user is reconnected
const openReconnectedLoginModal = () => {
reconnectedLoginModal.current?.show();
};

return (
<AppShell
navbar={
<Navbar width={{ base: 60 }} py={8}>
<Navbar.Section grow>
<Stack justify="center" align="center">
<NavbarLink
label="Search"
icon={IconSearch}
onClick={() => searchModal.current?.show()}
></NavbarLink>
<NavbarLink
label="Patients"
icon={IconUser}
onClick={() => navigate('/patient-details')}
active={location.pathname === '/patient-details'}
></NavbarLink>
<NavbarLink
label="Consultations"
icon={IconStethoscope}
></NavbarLink>
<NavbarLink label="Dispense" icon={IconPill}></NavbarLink>
</Stack>
</Navbar.Section>
</Navbar>
location.pathname === '/' ? undefined : (
<Navbar width={{ base: 60 }} py={8}>
<Navbar.Section grow>
<Stack justify="center" align="center">
<NavbarLink
label="Search"
icon={IconSearch}
onClick={() => searchModal.current?.show()}
></NavbarLink>
<NavbarLink
label="Patients"
icon={IconUser}
onClick={() => navigate('/patient-details')}
active={location.pathname === '/patient-details'}
></NavbarLink>
<NavbarLink
label="Consultations"
icon={IconStethoscope}
></NavbarLink>
<NavbarLink label="Dispense" icon={IconPill}></NavbarLink>
</Stack>
</Navbar.Section>
<Navbar.Section className="w-full justify-center">
<UserAvatar />
</Navbar.Section>
</Navbar>
)
}
>
<Outlet />
<SearchModal ref={searchModal} />
<ReconnectedLoginModal ref={reconnectedLoginModal} />
</AppShell>
);
}
Expand Down
34 changes: 34 additions & 0 deletions apps/frontend/src/app/login/auth0-provider/auth0-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React from 'react';
import { Auth0Provider as Auth0ReactProvider } from '@auth0/auth0-react';

type Auth0ProviderProps = {
children: React.ReactNode;
};

const AUTH0_CLIENT_ID =
process.env['NODE_ENV'] === 'development'
? process.env['NX_AUTH0_CLIENT_ID_DEV']
: process.env['NX_AUTH0_CLIENT_ID_PROD'];

const AUTH0_REDIRECT_URI =
process.env['NODE_ENV'] === 'development'
? process.env['NX_AUTH0_REDIRECT_URI_DEV']
: process.env['NX_AUTH0_REDIRECT_URI_PROD'];

export const Auth0Provider = (props: Auth0ProviderProps) => {
return (
<Auth0ReactProvider
domain={process.env['NX_AUTH0_DOMAIN']!}
clientId={AUTH0_CLIENT_ID!}
redirectUri={AUTH0_REDIRECT_URI!}
scope="openid"
responseType="token"
cacheLocation="localstorage"
>
{props.children}
</Auth0ReactProvider>
);
};

export default Auth0Provider;
1 change: 1 addition & 0 deletions apps/frontend/src/app/login/auth0-provider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './auth0-provider';
3 changes: 3 additions & 0 deletions apps/frontend/src/app/login/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './login-page';
export * from './auth0-provider';
export * from './reconnected-login-modal';
1 change: 1 addition & 0 deletions apps/frontend/src/app/login/login-page/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './login-page';
9 changes: 9 additions & 0 deletions apps/frontend/src/app/login/login-page/login-page.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { render } from '@testing-library/react';
import LoginPage from './login-page';

describe('Login Page', () => {
it('should render successfully', () => {
const { baseElement } = render(<LoginPage />);
expect(baseElement).toBeTruthy();
});
});
45 changes: 45 additions & 0 deletions apps/frontend/src/app/login/login-page/login-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { Group, Stack, Text, Title } from '@mantine/core';
import { Button, Logo } from '@shared';
import { useAuth0 } from '@auth0/auth0-react';
import { useNetwork } from '@shared';
import { OfflineLoginPage } from '../offline-login-page';
import { IconWifi } from '@tabler/icons';

export const LoginPage = () => {
const { loginWithRedirect } = useAuth0();
const { online } = useNetwork();

const handleClickLogin = () => {
loginWithRedirect();
};

if (!online) {
return <OfflineLoginPage />;
} else {
return (
<Stack className="items-center justify-center h-full m-auto w-1/4 2xl:w-1/6">
<Logo size="sm" />
<Title
color="blue.9"
order={3}
className="w-full font-extrabold text-center"
>
Welcome to SuperVision!
</Title>
<Group className="w-full flex-nowrap" spacing={0}>
<IconWifi size={36} className="mx-4" />
<Text>
Your device is online <br /> Please log in to continue to
SuperVision.
</Text>
</Group>
<Button uppercase fullWidth size="lg" onClick={handleClickLogin}>
Login with Auth0
</Button>
</Stack>
);
}
};

export default LoginPage;
1 change: 1 addition & 0 deletions apps/frontend/src/app/login/offline-login-page/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './offline-login-page';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import OfflineLoginPage from './offline-login-page';

describe('Offline Login Page', () => {
it('should render successfully', () => {
const { baseElement } = render(
<BrowserRouter>
<OfflineLoginPage />
</BrowserRouter>
);
expect(baseElement).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { Group, Stack, Text, TextInput, Title } from '@mantine/core';
import { Button, Logo } from '@shared';
import { useNavigate } from 'react-router-dom';
import { IconWifiOff } from '@tabler/icons';

export const OfflineLoginPage = () => {
const navigate = useNavigate();

const [email, setEmail] = React.useState('');
const [errorMessage, setErrorMessage] = React.useState<string | undefined>(
undefined
);

const navigateToPatientDetails = () => {
if (email === '') {
setErrorMessage('Please enter an email address');
} else if (
!email.endsWith('@auckland.ac.nz') &&
!email.endsWith('@aucklanduni.ac.nz')
) {
setErrorMessage('Invalid email - please ensure you enter a UoA email');
} else {
setErrorMessage(undefined);
sessionStorage.setItem('userEmail', email);
navigate('/patient-details');
}
};

return (
<Stack className="items-center justify-center h-full m-auto w-1/4 2xl:w-1/6">
<Logo size="sm" />
<Title
color="blue.9"
order={3}
className="w-full font-extrabold text-center"
>
Welcome to SuperVision!
</Title>
<Group className="w-full flex-nowrap" spacing={0}>
<IconWifiOff size={36} className="mx-4" />
<Text>
Your device is offline <br /> Please enter your university email
address to identify yourself.
</Text>
</Group>
<TextInput
label="Your email"
placeholder="Enter your email"
required
value={email}
onChange={(event) => setEmail(event.currentTarget.value)}
error={errorMessage}
className="w-full"
/>
<Button uppercase fullWidth size="lg" onClick={navigateToPatientDetails}>
Continue
</Button>
</Stack>
);
};

export default OfflineLoginPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './reconnected-login-modal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { render } from '@testing-library/react';
import ReconnectedLoginModal from './reconnected-login-modal';

describe('Reconnected Login Modal', () => {
it('should render successfully', () => {
const { baseElement } = render(<ReconnectedLoginModal />);
expect(baseElement).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useAuth0 } from '@auth0/auth0-react';
import { Modal, Text } from '@mantine/core';
import { Button } from '@shared';
import React, {
ForwardedRef,
forwardRef,
useImperativeHandle,
useState,
} from 'react';

export interface ReconnectedLoginModalRef {
show(): void;
}

export const ReconnectedLoginModal = forwardRef(
(props, ref: ForwardedRef<ReconnectedLoginModalRef>) => {
const { loginWithPopup } = useAuth0();
const [opened, setOpened] = useState(false);

useImperativeHandle(ref, () => ({
show() {
setOpened(true);
},
}));

const login = () => {
setOpened(false);
loginWithPopup();
};

return (
<Modal
title="Your internet connection was restored"
opened={opened}
onClose={() => setOpened(false)}
withCloseButton={false}
closeOnClickOutside={false}
closeOnEscape={false}
centered
classNames={{
title: 'font-extrabold',
}}
>
<Text className="-mt-2 pb-10">Please log in</Text>
<Button uppercase fullWidth size="lg" onClick={login}>
Login with Auth0
</Button>
</Modal>
);
}
);

export default ReconnectedLoginModal;
29 changes: 29 additions & 0 deletions apps/frontend/src/app/routes/protected-route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import { Navigate } from 'react-router-dom';
import { Center, Loader } from '@mantine/core';
import { useNetwork } from '@shared';

type ProtectedRouteProps = {
component: JSX.Element;
};

const ProtectedRoute = ({ component }: ProtectedRouteProps): JSX.Element => {
const { isLoading, isAuthenticated } = useAuth0();
const { online, isLoading: onlineStatusLoading } = useNetwork();
const userEmail = sessionStorage.getItem('userEmail');

if (isLoading || onlineStatusLoading) {
return (
<Center className="w-full h-full">
<Loader />
</Center>
);
} else if (isAuthenticated || (!online && userEmail)) {
return component;
} else {
return <Navigate to="/" />;
}
};

export default ProtectedRoute;
8 changes: 7 additions & 1 deletion apps/frontend/src/app/routes/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import React from 'react';
import App from 'app/app';
import { Route, Routes as RRRoutes } from 'react-router-dom';
import { PatientDetailsPage } from '@patients';
import { LoginPage } from '@login';
import ProtectedRoute from './protected-route';

const Routes = () => {
return (
<RRRoutes>
<Route path="/" element={<App />}>
<Route path="/patient-details" element={<PatientDetailsPage />} />
<Route index element={<LoginPage />} />
<Route
path="/patient-details"
element={<ProtectedRoute component={<PatientDetailsPage />} />}
/>
</Route>
</RRRoutes>
);
Expand Down
Loading

0 comments on commit aea9eba

Please sign in to comment.