Skip to content

Commit

Permalink
Merge branch 'dev' into 107-deploy
Browse files Browse the repository at this point in the history
  • Loading branch information
francisli committed Aug 27, 2024
2 parents 98e1344 + f779da0 commit 1b25de2
Show file tree
Hide file tree
Showing 51 changed files with 2,731 additions and 272 deletions.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@mantine/notifications": "^7.12.1",
"@tabler/icons-react": "^3.12.0",
"@tanstack/react-query": "^5.51.23",
"http-status-codes": "^2.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-qrcode-logo": "^3.0.0",
Expand Down
21 changes: 17 additions & 4 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ import PropTypes from 'prop-types';

import { Layout } from './stories/Layout/Layout';
import Index from './pages';
import Login from './pages/login/login';
import Register from './pages/register/register';
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 { AdminUsers } from './pages/admin/users/AdminUsers';

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';

const RedirectProps = {
isLoading: PropTypes.bool.isRequired,
Expand Down Expand Up @@ -103,9 +107,18 @@ function App() {
<Route
element={<Redirect isLoading={isLoading} isLoggedIn={isLoggedIn} />}
>
<Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} />
<Route path="/" element={<Index />} />
<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>
</>
Expand Down
18 changes: 15 additions & 3 deletions client/src/hooks/useAuthorization.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext } from 'react';
import { useContext, useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';

Expand All @@ -10,31 +10,42 @@ import Context from '../Context';
*
* @returns {{
* user: user,
* error: object,
* isLoading: boolean,
* handleLogin: (credentials: any) => Promise<void>,
* handleLogout: () => Promise<void>,
* }}
*/
export function useAuthorization() {
const { user, setUser } = useContext(Context);
const [error, setError] = useState(null);
const navigate = useNavigate();

const loginMutation = useMutation({
mutationFn: (credentials) => {
return fetch('/api/v1/auth/login', {
mutationFn: async (credentials) => {
return await fetch('/api/v1/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
credentials: 'include',
}).then((response) => {
if (!response.ok) {
return Promise.reject(response);
}
return response;
});
},
onSuccess: async (data, { redirectTo }) => {
const result = await data.json();
setUser(result);
navigate(redirectTo ?? '/');
},
onError: async (error) => {
const errorBody = await error.json();
setError({ ...errorBody, status: error.status });
},
});

const logoutMutation = useMutation({
Expand All @@ -57,6 +68,7 @@ export function useAuthorization() {

return {
user,
error,
isLoading: loginMutation.isPending || logoutMutation.isPending,
handleLogin,
handleLogout,
Expand Down
27 changes: 27 additions & 0 deletions client/src/pages/auth/form.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.header {
text-align: center;
margin: 2rem 0;
}

.header > p {
padding: 0 2rem;
}

.form {
display: flex;
flex-direction: column;
gap: 1rem;
}

.formCompletion {
background: #ebfbee;
padding: 0.1rem 1rem;
color: #2b8a3e;
font-size: small;
}

@media (min-width: 48em) {
.header {
margin: 8.75rem 0 2rem 0;
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import {
TextInput,
Expand Down Expand Up @@ -78,7 +79,9 @@ export function LoginForm({
Log in
</Button>
<div className={classes.anchor}>
<Anchor href="/forgot-password">Forgot password</Anchor>
<Anchor component={Link} to="/password/forgot">
Forgot password
</Anchor>
</div>
</Container>
</form>
Expand Down
69 changes: 69 additions & 0 deletions client/src/pages/auth/login/login.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { StatusCodes } from 'http-status-codes';

import { useAuthorization } from '../../../hooks/useAuthorization';
import { LoginForm } from './LoginForm';

import classes from './login.module.css';

/**
* Login page component.
*/
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [emailError, setEmailError] = useState(null);
const [passwordError, setPasswordError] = useState(null);
const { handleLogin, error } = useAuthorization();
const location = useLocation();

const login = async () => {
setEmailError(null);
setPasswordError(null);
const { redirectTo } = location.state ?? {};
await handleLogin({ email, password, redirectTo });
};

useEffect(() => {
if (error && error.status != StatusCodes.OK) {
switch (error.status) {
case StatusCodes.NOT_FOUND:
setEmailError('The email you entered isn’t connected to an account.');
break;
case StatusCodes.UNAUTHORIZED:
setPasswordError('The password you’ve entered is incorrect.');
break;
case StatusCodes.FORBIDDEN:
setEmailError(error.message);
break;
default:
setEmailError(`${error.status}: ${error.message}`);
break;
}
}
}, [error]);

return (
<div>
<h2 className={classes.header}>Login</h2>
<LoginForm
email={email}
onEmailChange={(event) => {
setEmailError(null);
setEmail(event.target.value);
}}
emailError={emailError}
password={password}
onPasswordChange={(event) => {
setPasswordError(null);
setPassword(event.target.value);
}}
passwordError={passwordError}
onLogin={login}
/>
</div>
);
}

export default Login;
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
.login {
width: 100vw;
height: 100vh;
height: 100dvh;
}

.banner {
display: none;
}

.header {
text-align: center;
margin: 2rem 0;
Expand All @@ -28,25 +18,9 @@
}

@media (min-width: 48em) {
.login {
display: flex;
height: 100vh;

& > * {
flex: 1;
}
}

.header {
margin: 8.75rem 0 2rem 0;
}

.banner {
display: block;
width: 100%;
height: 100%;
background-color: aqua;
}
}

.loginBtn {
Expand Down
65 changes: 65 additions & 0 deletions client/src/pages/auth/password-forgot/PasswordForgotForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import { Button, Container, Loader, TextInput } from '@mantine/core';
import PropTypes from 'prop-types';
import classes from '../form.module.css';

const formProps = {
email: PropTypes.string.isRequired,
emailError: PropTypes.string,
onEmailChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
formState: PropTypes.number.isRequired,
};
/**
* Reset Form component for Password Reset page
* @param {PropTypes.InferProps<typeof formProps>} props
*/
export function PasswordForgotForm({
email,
emailError,
onEmailChange,
onSubmit,
isLoading,
formState,
}) {
return (
<form
onSubmit={(event) => {
event.preventDefault();
onSubmit();
}}
>
<Container size="25rem" className={classes.form}>
<TextInput
disabled={isLoading || formState == 2}
name="email"
label="Email"
placeholder="user@email.com"
value={email}
onChange={onEmailChange}
error={emailError}
/>
{formState == 1 && (
<Button type="submit" disabled={!email.length}>
{isLoading ? <Loader size={20} /> : 'Send Password Reset'}
</Button>
)}
{formState == 2 && (
<Container size="25rem" styles={{ root: { padding: 0 } }}>
<div className={classes.formCompletion}>
<p>
Form Complete! You will receive a confirmation email shortly.
Acceptance into SF life line will also be sent via the email
address you submitted.
</p>
<p>Save this site to your browser window for convenience.</p>
</div>
</Container>
)}
</Container>
</form>
);
}

PasswordForgotForm.propTypes = formProps;
69 changes: 69 additions & 0 deletions client/src/pages/auth/password-forgot/passwordForgot.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useState } from 'react';
import classes from '../form.module.css';
import { Flex } from '@mantine/core';
import { PasswordForgotForm } from './PasswordForgotForm';

/**
* Password reset page
*/
function PasswordForgot() {
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [formState, setFormState] = useState(1);

/**
* API call to send a request for a password reset to email on account profile
*/
function sendPasswordReset() {
setEmailError(null);
setIsLoading(true);
fetch('/api/v1/auth/password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: email }),
})
.then((response) => {
if (!response.ok) {
return Promise.reject(response);
}
setFormState(2);
})
.catch((error) => {
error.json().then(({ message }) => {
setEmailError(message);
});
})
.finally(() => {
setIsLoading(false);
});
}
return (
<div>
<div className={classes.header}>
<h2>Forgot Password</h2>
<p>
Enter the email associated to your account and we will send a reset
password link
</p>
</div>
<Flex direction="column" gap="md">
<PasswordForgotForm
email={email}
emailError={emailError}
onEmailChange={(event) => {
setEmailError(null);
setEmail(event.target.value);
}}
isLoading={isLoading}
onSubmit={sendPasswordReset}
formState={formState}
/>
</Flex>
</div>
);
}

export default PasswordForgot;
Loading

0 comments on commit 1b25de2

Please sign in to comment.