Skip to content

Commit 8bed4d5

Browse files
committed
Add password reset page
1 parent bcd58a7 commit 8bed4d5

10 files changed

+233
-5
lines changed

apps/api/src/auth/auth.controller.ts

-2
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,12 @@ export class AuthController {
8383
}
8484

8585
@Post('reset-password/start')
86-
@UseGuards(AuthenticatedGuard)
8786
async startPasswordReset(@Query('email') email: string): Promise<any> {
8887
await this.authService.sendPasswordResetEmail(email);
8988
return { statusCode: 200 };
9089
}
9190

9291
@Post('reset-password/reset')
93-
@UseGuards(AuthenticatedGuard)
9492
async resetPassword(@Body() dto: ResetPasswordDto): Promise<any> {
9593
const userId = new Types.ObjectId(dto.user);
9694
await this.authService.resetUserPassword(userId, dto.token, dto.password);

apps/api/src/auth/templates/password-reset.template.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export class PasswordRestTemplate implements EmailTemplate {
1111
) {}
1212

1313
toString(): string {
14-
const url = `${BASE_URL}reset-password?user=${this.userId.toString()}&token=${this.token}`;
14+
const url = `${BASE_URL}password-reset?user=${this.userId.toString()}&token=${this.token}`;
1515
return (
1616
`Hello,` +
1717
`\r\n` +
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { BsCheckCircle } from 'react-icons/bs';
2+
3+
function PasswordResetSuccessAlert() {
4+
return (
5+
<div className="flex flex-col items-center gap-6 rounded-xl border border-gray-300 px-8 py-12">
6+
<div className="flex flex-col items-center justify-center gap-6 text-center text-muted">
7+
<BsCheckCircle className="text-5xl text-success" /> We&apos;ve sent password reset
8+
instructions to the e-mail address you have provided. If you haven&apos;t received this
9+
email in few minutes, please check your spam folder.
10+
</div>
11+
</div>
12+
);
13+
}
14+
export default PasswordResetSuccessAlert;
Loading

apps/client/src/components/Form/FancyInput.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface FancyInputProps extends InputProps {
1212
icon: IconType;
1313
}
1414

15-
const FancyInput = forwardRef(function TextInput(
15+
const FancyInput = forwardRef(function FancyInput(
1616
{ label, icon, ...props }: FancyInputProps,
1717
ref: any,
1818
) {

apps/client/src/components/LoginForm.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ function LoginForm() {
6565
autoComplete="on"
6666
/>
6767
<Link
68-
to="password-reset"
68+
to="/password-reset"
6969
className="block text-right text-sm text-muted"
7070
>
7171
Forgot Password?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useSearchParams } from 'react-router-dom';
2+
import UndrawPasswordReset from '../../assets/undraw_password_reset.svg';
3+
import FullFocusContainer from '../FullFocusContainer';
4+
import ResetPasswordForm from '../ResetPasswordForm';
5+
import SendPasswordResetEmailForm from '../SendPasswordResetEmailForm';
6+
import SideBySideImage from '../SideBySideImage';
7+
8+
function PasswordResetPage() {
9+
const [searchParams] = useSearchParams();
10+
const user = searchParams.get('user');
11+
const token = searchParams.get('token');
12+
13+
return (
14+
<FullFocusContainer>
15+
<SideBySideImage
16+
imageSrc={UndrawPasswordReset}
17+
className="flex-1"
18+
>
19+
<h2 className="mb-6 text-center text-4xl">Reset password</h2>
20+
{user && token ? (
21+
<ResetPasswordForm
22+
user={user}
23+
token={token}
24+
/>
25+
) : (
26+
<SendPasswordResetEmailForm />
27+
)}
28+
</SideBySideImage>
29+
</FullFocusContainer>
30+
);
31+
}
32+
export default PasswordResetPage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import axios, { AxiosError } from 'axios';
2+
import { useContext, useState } from 'react';
3+
import { useForm } from 'react-hook-form';
4+
import { BsShieldLock } from 'react-icons/bs';
5+
import { IResetPasswordDto } from 'shared-types';
6+
import { UserContext } from '../context/UserContext';
7+
import { Utils } from '../utils/utils';
8+
import Button from './Button';
9+
import FancyInput from './Form/FancyInput';
10+
import Alert from './Helpers/Alert';
11+
import { useNavigate } from 'react-router-dom';
12+
import toast from 'react-hot-toast';
13+
14+
export interface ResetPasswordFormProps {
15+
user: string;
16+
token: string;
17+
}
18+
19+
type Inputs = {
20+
password: string;
21+
passwordConfirm: string;
22+
};
23+
24+
function ResetPasswordForm({ user, token }: ResetPasswordFormProps) {
25+
const { register, handleSubmit } = useForm<Inputs>();
26+
const { isAuthenticated, logout } = useContext(UserContext);
27+
28+
const [loading, setLoading] = useState(false);
29+
const [error, setError] = useState<string | null>(null);
30+
31+
const navigate = useNavigate();
32+
33+
function onSubmit(inputs: Inputs) {
34+
setLoading(true);
35+
setError(null);
36+
37+
const dto: IResetPasswordDto = {
38+
user,
39+
token,
40+
password: inputs.password,
41+
};
42+
axios
43+
.post(`/api/auth/reset-password/reset`, dto)
44+
.then(() => {
45+
toast.success('Your password have been changed, you can login now');
46+
if (isAuthenticated) {
47+
logout();
48+
}
49+
50+
navigate('/login');
51+
})
52+
.catch((error: AxiosError) => {
53+
setError(Utils.requestErrorToString(error));
54+
})
55+
.finally(() => setLoading(false));
56+
}
57+
58+
return (
59+
<form onSubmit={handleSubmit(onSubmit)}>
60+
{error && <Alert>{error}</Alert>}
61+
62+
<FancyInput
63+
label="New password"
64+
type="password"
65+
minLength={4}
66+
maxLength={32}
67+
placeholder="Type your new password"
68+
{...register('password', { required: true })}
69+
icon={BsShieldLock}
70+
disabled={loading}
71+
/>
72+
<FancyInput
73+
label="Confirm password"
74+
type="password"
75+
placeholder="Re-type your password"
76+
{...register('passwordConfirm', { required: true })}
77+
icon={BsShieldLock}
78+
disabled={loading}
79+
/>
80+
81+
<Button
82+
className="mt-8 w-full text-lg"
83+
type="submit"
84+
loading={loading}
85+
>
86+
Reset password
87+
</Button>
88+
</form>
89+
);
90+
}
91+
export default ResetPasswordForm;

apps/client/src/components/Router.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import RegisterForm from './RegisterForm';
4444
import RegisterSelect from './RegisterSelect';
4545
import { ProtectedRoute, PublicRoute } from './SpecialRoutes';
4646
import EmailConfirmPage from './Pages/EmailConfirmPage';
47+
import PasswordResetPage from './Pages/PasswordResetPage';
4748

4849
function Router() {
4950
const router = createBrowserRouter([
@@ -89,6 +90,11 @@ function Router() {
8990
element: <EmailConfirmPage />,
9091
errorElement: <ApplicationError />,
9192
},
93+
{
94+
path: 'password-reset',
95+
element: <PasswordResetPage />,
96+
errorElement: <ApplicationError />,
97+
},
9298
{
9399
path: 'dashboard',
94100
element: (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import axios, { AxiosError } from 'axios';
2+
import { useContext, useState } from 'react';
3+
import { useForm } from 'react-hook-form';
4+
import { BsCheckCircle, BsEnvelopeAt, BsPerson, BsShieldLock } from 'react-icons/bs';
5+
import { Link } from 'react-router-dom';
6+
import { IResetPasswordDto, UserLoginDto } from 'shared-types';
7+
import { UserContext } from '../context/UserContext';
8+
import { Utils } from '../utils/utils';
9+
import Button from './Button';
10+
import FancyInput from './Form/FancyInput';
11+
import Alert from './Helpers/Alert';
12+
import PasswordResetSuccessAlert from '../PasswordResetSuccessAlert';
13+
14+
type Inputs = {
15+
email: string;
16+
};
17+
18+
function SendPasswordResetEmailForm() {
19+
const { register, handleSubmit } = useForm<Inputs>();
20+
21+
const [loading, setLoading] = useState(false);
22+
const [error, setError] = useState<string | null>(null);
23+
const [success, setSuccess] = useState(false);
24+
25+
function onSubmit(inputs: Inputs) {
26+
setLoading(true);
27+
setError(null);
28+
29+
axios
30+
.post(`/api/auth/reset-password/start?email=${inputs.email}`)
31+
.then(() => setSuccess(true))
32+
.catch((error: AxiosError) => {
33+
setError(Utils.requestErrorToString(error));
34+
})
35+
.finally(() => setLoading(false));
36+
}
37+
38+
return (
39+
<form onSubmit={handleSubmit(onSubmit)}>
40+
{success ? (
41+
<PasswordResetSuccessAlert />
42+
) : (
43+
<>
44+
<p className="mb-4 text-center text-muted">
45+
Please enter the E-mail address that you have used to register to StockedUp. We will
46+
send you a link that you can use to reset your password
47+
</p>
48+
49+
{error && <Alert>{error}</Alert>}
50+
51+
<FancyInput
52+
label="E-Mail"
53+
placeholder="Type your e-mail address"
54+
type="email"
55+
{...register('email', { required: true })}
56+
icon={BsEnvelopeAt}
57+
disabled={loading}
58+
autoFocus
59+
/>
60+
61+
<div className="mt-10 flex gap-6">
62+
<Link to="/login">
63+
<Button
64+
className="text-lg"
65+
variant="secondary-outline"
66+
type="submit"
67+
loading={loading}
68+
>
69+
Cancel
70+
</Button>
71+
</Link>
72+
73+
<Button
74+
className="flex-1 text-lg"
75+
type="submit"
76+
loading={loading}
77+
>
78+
Reset password
79+
</Button>
80+
</div>
81+
</>
82+
)}
83+
</form>
84+
);
85+
}
86+
export default SendPasswordResetEmailForm;

0 commit comments

Comments
 (0)