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

[#93} User can reset password #106

Merged
merged 14 commits into from
Aug 27, 2024
Merged
22 changes: 16 additions & 6 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ 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 = {
Expand Down Expand Up @@ -105,10 +108,17 @@ function App() {
element={<Redirect isLoading={isLoading} isLoggedIn={isLoggedIn} />}
>
<Route path="/" element={<Index />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/register/:inviteId" element={<Register />} />
<Route path="verify/:emailVerificationToken" element={<Verify />} />
<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
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;
}
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { StatusCodes } from 'http-status-codes';

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

import classes from './login.module.css';
Expand Down Expand Up @@ -45,26 +45,23 @@ function Login() {
}, [error]);

return (
<div className={classes.login}>
<div className={classes.banner}>Image goes here</div>
<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>
<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>
);
}
Expand Down
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;
56 changes: 56 additions & 0 deletions client/src/pages/auth/password-forgot/passwordForgot.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import '@mantine/core/styles.css';
import { PasswordForgotForm } from './PasswordForgotForm';

export default {
title: 'Password Forgot Form',
component: PasswordForgotForm,
tags: ['autodocs'],
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
};

export const Default = {
args: {
email: '',
emailError: null,
isLoading: false,
onEmailChange: () => {},
onSubmit: () => {},
formState: 1,
},
};

export const Loading = {
args: {
email: 'user@test.com',
emailError: null,
isLoading: true,
onEmailChange: () => {},
onSubmit: () => {},
formState: 1,
},
};

export const Error = {
args: {
email: 'troll@test.com',
emailError: 'Email not found in SF Life Line Database',
isLoading: false,
onEmailChange: () => {},
onSubmit: () => {},
formState: 1,
},
};

export const Success = {
args: {
email: 'user@test.com',
emailError: null,
isLoading: false,
onEmailChange: () => {},
onSubmit: () => {},
formState: 2,
},
};
Loading