Skip to content

Commit 358ba9b

Browse files
author
Emile Frey
committed
added a forgot password/reset password mechanism with django console email backend, updated README with info on these changes
1 parent 1dc0249 commit 358ba9b

File tree

12 files changed

+321
-43
lines changed

12 files changed

+321
-43
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ docker-compose -f "docker-compose.yml" up -d --build
3636
```
3737
The server should be available at `http://127.0.0.1/`. This mode will not hot reload since it's running a production build (npm build).
3838

39+
The password reset feature is fully functional. In order to get the password reset url, you will need to open the backend django console. Enter the following in an application like PowerShell:
40+
41+
```sh
42+
$id = $(docker ps -aqf "name=django-react-postgres-boilerplate_backend")
43+
docker logs --tail 1000 -f $id
44+
```
45+
46+
Upon submitting a valid email, you should get a path like /password_reset?token=abcdefgxyz123; paste this in your browser after localhost:3000 to access the password reset form. The password reset form validates the token and allows the user to provide a new password.
47+
48+
Check out the Django docs starting [here](https://docs.djangoproject.com/en/3.1/topics/email/#smtp-backend) in order to update the Email Backend from a console output to an actual SMTP backend.
3949

4050
**_Suggestions/feedback in discussions tab are greatly appreciated._**
4151

@@ -49,9 +59,9 @@ The server should be available at `http://127.0.0.1/`. This mode will not hot re
4959
- [x] show password errors
5060
- [x] loading icon on login
5161
- [x] ensure a non-existing route redirects to home
52-
- [ ] email support (for password reset)
62+
- [x] email support (for password reset)
63+
- [x] forgot password functionality (email)
5364
- [ ] Add support for nested sub-routes off the main left-nav routes
54-
- [ ] forgot password functionality (email)
5565
- [ ] Context level modal?
5666
- [ ] Swagger API Explorer
5767
- [ ] Backend Testing

backend/mainapp/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
'django.contrib.staticfiles',
4040

4141
'rest_framework',
42+
'django_rest_passwordreset',
4243
'rest_framework.authtoken',
4344
'rest_auth',
4445
'corsheaders',
@@ -78,6 +79,8 @@
7879

7980
WSGI_APPLICATION = 'mainapp.wsgi.application'
8081

82+
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
83+
8184

8285
# Database
8386
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases

backend/mainapp/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@
2020
path('admin/', admin.site.urls),
2121
path('api/auth/', include('users.urls')),
2222
path('api/', include('helloyou.urls')),
23+
path('api/password_reset/', include('django_rest_passwordreset.urls', namespace='password_reset')),
2324
]

backend/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ djangorestframework==3.11.1
33
django-rest-auth==0.9.5
44
django-cors-headers==3.5.0
55
psycopg2-binary==2.8.5
6-
gunicorn==20.0.4
6+
gunicorn==20.0.4
7+
django-rest-passwordreset

backend/users/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# __init__.py
2+
import users.signals

backend/users/signals.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.dispatch import receiver
2+
from django.urls import reverse
3+
from django_rest_passwordreset.signals import reset_password_token_created
4+
from django.core.mail import send_mail
5+
6+
@receiver(reset_password_token_created)
7+
def password_reset_token_created(sender, instance, reset_password_token, *args, **kwargs):
8+
9+
# email_plaintext_message = "{}?token={}".format(reverse('password_reset:reset-password-request'), reset_password_token.key)
10+
email_plaintext_message = "{}?token={}".format('/password_reset/', reset_password_token.key)
11+
print(email_plaintext_message)
12+
print("!" * 50)
13+
send_mail(
14+
# title:
15+
"Password Reset for {title}".format(title="HelloYou"),
16+
# message:
17+
email_plaintext_message,
18+
# from:
19+
"noreply@somehost.local",
20+
# to:
21+
[reset_password_token.user.email]
22+
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, { useRef, useState } from 'react'
2+
import { FormHelperText, Button, TextField } from '@material-ui/core';
3+
import axios from 'axios';
4+
import { useStyles } from './Login'
5+
import * as settings from '../../settings';
6+
7+
export const ForgotPassword = () => {
8+
const emailInput = useRef<HTMLInputElement>(null);
9+
const [submitted, setSubmitted] = useState(false)
10+
const [feedback, setFeedback] = useState("Please enter the email associated with your user account.")
11+
const classes = useStyles();
12+
13+
const submitEmail = (e: React.FormEvent<HTMLFormElement>) => {
14+
e.preventDefault();
15+
if (emailInput.current !== null) {
16+
axios.post(`${settings.API_SERVER}/api/password_reset/reset_password/`, { email: emailInput.current.value })
17+
.then((response: any) => {
18+
setSubmitted(true)
19+
if (response.status === 200) {
20+
setFeedback('Thank you! If an account is associated with the email you provided, you will receive an email with instructions to reset your password.');
21+
}
22+
})
23+
.catch((error: any) => setFeedback('Error! Be sure to enter a valid email address.')
24+
)
25+
}
26+
}
27+
28+
29+
return (
30+
submitted ? <div style={{color: 'black'}}>{feedback}</div> :
31+
<form onSubmit={submitEmail} className={classes.form} >
32+
<FormHelperText
33+
id="email-helper-text"
34+
className={classes.helper}
35+
>
36+
{feedback}
37+
</FormHelperText>
38+
<TextField
39+
variant="outlined"
40+
required
41+
fullWidth
42+
id="email"
43+
label="Email Address"
44+
name="email"
45+
autoComplete="email"
46+
autoFocus
47+
inputRef={emailInput}
48+
/>
49+
<Button
50+
id="password-reset-submit-button"
51+
type="submit"
52+
fullWidth
53+
variant="contained"
54+
color="primary"
55+
className={classes.submit}
56+
>
57+
Submit
58+
</Button>
59+
</form>
60+
61+
)
62+
}

frontend/src/components/Login/Login.tsx

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ import * as actions from '../../auth/authActions';
1515
import { useHistory, useLocation } from "react-router-dom";
1616
import { AppProps } from '../../App';
1717
import ValidationMessages from '../../helpers/ValidationMessages'
18-
import { LinearProgress } from '@material-ui/core';
18+
import { Grid, LinearProgress, Link } from '@material-ui/core';
1919
import { APP_NAME } from '../../settings'
20+
import { ForgotPassword } from './ForgotPassword'
2021

21-
const useStyles = makeStyles((theme) => ({
22+
export const useStyles = makeStyles((theme) => ({
2223
paper: {
2324
marginTop: theme.spacing(8),
2425
display: 'flex',
@@ -36,6 +37,9 @@ const useStyles = makeStyles((theme) => ({
3637
submit: {
3738
margin: theme.spacing(3, 0, 2),
3839
},
40+
helper: {
41+
margin: theme.spacing(1),
42+
},
3943
title: {
4044
marginTop: theme.spacing(2),
4145
marginBottom: theme.spacing(2),
@@ -56,6 +60,7 @@ function Login(props: AppProps) {
5660
const classes = useStyles();
5761
const [username, setuserName] = useState("");
5862
const [password, setPassword] = useState("");
63+
const [passwordReset, setPasswordReset] = useState(false);
5964
const [validationErrors, setValidationErrors] = useState<Record<string, string[]>>({})
6065
const [isLoading, setIsLoading] = useState(false)
6166

@@ -96,45 +101,56 @@ function Login(props: AppProps) {
96101
<LockOutlinedIcon />
97102
</Avatar>
98103
<Typography component="h1" variant="h5">
99-
Sign in
104+
{passwordReset ? "Reset Password" : "Sign in"}
100105
</Typography>
101-
<form className={classes.form} noValidate onSubmit={handleSubmit}>
102-
<TextField
103-
variant="outlined"
104-
margin="normal"
105-
required
106-
fullWidth
107-
id="username"
108-
label="User Name"
109-
name="username"
110-
autoComplete="username"
111-
autoFocus
112-
onChange={handleFormFieldChange}
113-
/>
114-
<TextField
115-
variant="outlined"
116-
margin="normal"
117-
required
118-
fullWidth
119-
name="password"
120-
label="Password"
121-
type="password"
122-
id="password"
123-
autoComplete="current-password"
124-
onChange={handleFormFieldChange}
125-
/>
126-
<ValidationMessages validationErrors={validationErrors}/>
127-
{isLoading && <LinearProgress color="secondary" />}
128-
<Button
129-
type="submit"
130-
fullWidth
131-
variant="contained"
132-
color="primary"
133-
className={classes.submit}
134-
>
135-
Sign In
106+
{passwordReset ? <ForgotPassword /> :
107+
<form className={classes.form} noValidate onSubmit={handleSubmit}>
108+
<TextField
109+
variant="outlined"
110+
margin="normal"
111+
required
112+
fullWidth
113+
id="username"
114+
label="User Name"
115+
name="username"
116+
autoComplete="username"
117+
autoFocus
118+
onChange={handleFormFieldChange}
119+
/>
120+
<TextField
121+
variant="outlined"
122+
margin="normal"
123+
required
124+
fullWidth
125+
name="password"
126+
label="Password"
127+
type="password"
128+
id="password"
129+
autoComplete="current-password"
130+
onChange={handleFormFieldChange}
131+
/>
132+
<ValidationMessages validationErrors={validationErrors} />
133+
{isLoading && <LinearProgress color="secondary" />}
134+
<Button
135+
type="submit"
136+
fullWidth
137+
variant="contained"
138+
color="primary"
139+
className={classes.submit}
140+
>
141+
Sign In
136142
</Button>
137-
</form>
143+
</form>
144+
}
145+
<Grid container justify="center">
146+
<Grid item xs={12}>
147+
<Grid container justify="center">
148+
<Link onClick={() => setPasswordReset(!passwordReset)}>
149+
{passwordReset ? 'Back to Login' : 'Forgot password?'}
150+
</Link>
151+
</Grid>
152+
</Grid>
153+
</Grid>
138154
</div>
139155
</Container>
140156
);

0 commit comments

Comments
 (0)