Skip to content

Commit f10e11e

Browse files
Chapter 8: Password resets (8g)
1 parent b7c4701 commit f10e11e

File tree

8 files changed

+117
-1
lines changed

8 files changed

+117
-1
lines changed

app/auth/forms.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,16 @@ class ChangePasswordForm(FlaskForm):
4242
password2 = PasswordField('Confirm new password',
4343
validators=[DataRequired()])
4444
submit = SubmitField('Update Password')
45+
46+
47+
class PasswordResetRequestForm(FlaskForm):
48+
email = StringField('Email', validators=[DataRequired(), Length(1, 64),
49+
Email()])
50+
submit = SubmitField('Reset Password')
51+
52+
53+
class PasswordResetForm(FlaskForm):
54+
password = PasswordField('New Password', validators=[
55+
DataRequired(), EqualTo('password2', message='Passwords must match')])
56+
password2 = PasswordField('Confirm password', validators=[DataRequired()])
57+
submit = SubmitField('Reset Password')

app/auth/views.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from .. import db
66
from ..models import User
77
from ..email import send_email
8-
from .forms import LoginForm, RegistrationForm, ChangePasswordForm
8+
from .forms import LoginForm, RegistrationForm, ChangePasswordForm,\
9+
PasswordResetRequestForm, PasswordResetForm
910

1011

1112
@auth.before_app_request
@@ -102,3 +103,36 @@ def change_password():
102103
else:
103104
flash('Invalid password.')
104105
return render_template("auth/change_password.html", form=form)
106+
107+
108+
@auth.route('/reset', methods=['GET', 'POST'])
109+
def password_reset_request():
110+
if not current_user.is_anonymous:
111+
return redirect(url_for('main.index'))
112+
form = PasswordResetRequestForm()
113+
if form.validate_on_submit():
114+
user = User.query.filter_by(email=form.email.data.lower()).first()
115+
if user:
116+
token = user.generate_reset_token()
117+
send_email(user.email, 'Reset Your Password',
118+
'auth/email/reset_password',
119+
user=user, token=token)
120+
flash('An email with instructions to reset your password has been '
121+
'sent to you.')
122+
return redirect(url_for('auth.login'))
123+
return render_template('auth/reset_password.html', form=form)
124+
125+
126+
@auth.route('/reset/<token>', methods=['GET', 'POST'])
127+
def password_reset(token):
128+
if not current_user.is_anonymous:
129+
return redirect(url_for('main.index'))
130+
form = PasswordResetForm()
131+
if form.validate_on_submit():
132+
if User.reset_password(token, form.password.data):
133+
db.session.commit()
134+
flash('Your password has been updated.')
135+
return redirect(url_for('auth.login'))
136+
else:
137+
return redirect(url_for('main.index'))
138+
return render_template('auth/reset_password.html', form=form)

app/models.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ def confirm(self, token):
5151
db.session.add(self)
5252
return True
5353

54+
def generate_reset_token(self, expiration=3600):
55+
s = Serializer(current_app.config['SECRET_KEY'], expiration)
56+
return s.dumps({'reset': self.id}).decode('utf-8')
57+
58+
@staticmethod
59+
def reset_password(token, new_password):
60+
s = Serializer(current_app.config['SECRET_KEY'])
61+
try:
62+
data = s.loads(token.encode('utf-8'))
63+
except:
64+
return False
65+
user = User.query.get(data.get('reset'))
66+
if user is None:
67+
return False
68+
user.password = new_password
69+
db.session.add(user)
70+
return True
71+
5472
def __repr__(self):
5573
return '<User %r>' % self.username
5674

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<p>Dear {{ user.username }},</p>
2+
<p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p>
3+
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
4+
<p>{{ url_for('auth.password_reset', token=token, _external=True) }}</p>
5+
<p>If you have not requested a password reset simply ignore this message.</p>
6+
<p>Sincerely,</p>
7+
<p>The Flasky Team</p>
8+
<p><small>Note: replies to this email address are not monitored.</small></p>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Dear {{ user.username }},
2+
3+
To reset your password click on the following link:
4+
5+
{{ url_for('auth.password_reset', token=token, _external=True) }}
6+
7+
If you have not requested a password reset simply ignore this message.
8+
9+
Sincerely,
10+
11+
The Flasky Team
12+
13+
Note: replies to this email address are not monitored.

app/templates/auth/login.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ <h1>Login</h1>
1010
<div class="col-md-4">
1111
{{ wtf.quick_form(form) }}
1212
<br>
13+
<p>Forgot your password? <a href="{{ url_for('auth.password_reset_request') }}">Click here to reset it</a>.</p>
1314
<p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p>
1415
</div>
1516
{% endblock %}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% extends "base.html" %}
2+
{% import "bootstrap/wtf.html" as wtf %}
3+
4+
{% block title %}Flasky - Password Reset{% endblock %}
5+
6+
{% block page_content %}
7+
<div class="page-header">
8+
<h1>Reset Your Password</h1>
9+
</div>
10+
<div class="col-md-4">
11+
{{ wtf.quick_form(form) }}
12+
</div>
13+
{% endblock %}

tests/test_user_model.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,19 @@ def test_expired_confirmation_token(self):
5858
token = u.generate_confirmation_token(1)
5959
time.sleep(2)
6060
self.assertFalse(u.confirm(token))
61+
62+
def test_valid_reset_token(self):
63+
u = User(password='cat')
64+
db.session.add(u)
65+
db.session.commit()
66+
token = u.generate_reset_token()
67+
self.assertTrue(User.reset_password(token, 'dog'))
68+
self.assertTrue(u.verify_password('dog'))
69+
70+
def test_invalid_reset_token(self):
71+
u = User(password='cat')
72+
db.session.add(u)
73+
db.session.commit()
74+
token = u.generate_reset_token()
75+
self.assertFalse(User.reset_password(token + 'a', 'horse'))
76+
self.assertTrue(u.verify_password('cat'))

0 commit comments

Comments
 (0)