Skip to content

Commit

Permalink
Chapter 8: Password resets (8g)
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Jun 9, 2019
1 parent 74eb936 commit 4711144
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 1 deletion.
13 changes: 13 additions & 0 deletions app/auth/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,16 @@ class ChangePasswordForm(FlaskForm):
password2 = PasswordField('Confirm new password',
validators=[DataRequired()])
submit = SubmitField('Update Password')


class PasswordResetRequestForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64),
Email()])
submit = SubmitField('Reset Password')


class PasswordResetForm(FlaskForm):
password = PasswordField('New Password', validators=[
DataRequired(), EqualTo('password2', message='Passwords must match')])
password2 = PasswordField('Confirm password', validators=[DataRequired()])
submit = SubmitField('Reset Password')
36 changes: 35 additions & 1 deletion app/auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from .. import db
from ..models import User
from ..email import send_email
from .forms import LoginForm, RegistrationForm, ChangePasswordForm
from .forms import LoginForm, RegistrationForm, ChangePasswordForm,\
PasswordResetRequestForm, PasswordResetForm


@auth.before_app_request
Expand Down Expand Up @@ -102,3 +103,36 @@ def change_password():
else:
flash('Invalid password.')
return render_template("auth/change_password.html", form=form)


@auth.route('/reset', methods=['GET', 'POST'])
def password_reset_request():
if not current_user.is_anonymous:

This comment has been minimized.

Copy link
@liwu-alt

liwu-alt Nov 27, 2019

that need add not ?

This comment has been minimized.

Copy link
@wangye1221

wangye1221 Mar 18, 2020

that need add not ?

add in "app/models.py"

This comment has been minimized.

Copy link
@ezebunandu

ezebunandu May 18, 2020

Yeah. I'm thinking there should be no "not" here too. We want to return to index if the current user is anonymous, but give them the reset form otherwise.

This comment has been minimized.

Copy link
@miguelgrinberg

miguelgrinberg May 18, 2020

Author Owner

No, it's the reverse. If the user is trying to reset their password, then obviously they must be anonymous. If they are not anonymous that means they are logged in, so they wouldn't be resetting their password.

This comment has been minimized.

Copy link
@ezebunandu

ezebunandu May 18, 2020

Oh. That totally makes sense. Thanks Miguel.

return redirect(url_for('main.index'))
form = PasswordResetRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data.lower()).first()
if user:
token = user.generate_reset_token()
send_email(user.email, 'Reset Your Password',
'auth/email/reset_password',
user=user, token=token)
flash('An email with instructions to reset your password has been '
'sent to you.')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password.html', form=form)


@auth.route('/reset/<token>', methods=['GET', 'POST'])
def password_reset(token):
if not current_user.is_anonymous:
return redirect(url_for('main.index'))
form = PasswordResetForm()
if form.validate_on_submit():
if User.reset_password(token, form.password.data):
db.session.commit()
flash('Your password has been updated.')
return redirect(url_for('auth.login'))
else:
return redirect(url_for('main.index'))
return render_template('auth/reset_password.html', form=form)
18 changes: 18 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ def confirm(self, token):
db.session.add(self)
return True

def generate_reset_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'reset': self.id}).decode('utf-8')

@staticmethod
def reset_password(token, new_password):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except:
return False
user = User.query.get(data.get('reset'))
if user is None:
return False
user.password = new_password
db.session.add(user)
return True

def __repr__(self):
return '<User %r>' % self.username

Expand Down
8 changes: 8 additions & 0 deletions app/templates/auth/email/reset_password.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<p>Dear {{ user.username }},</p>
<p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.password_reset', token=token, _external=True) }}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>
13 changes: 13 additions & 0 deletions app/templates/auth/email/reset_password.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Dear {{ user.username }},

To reset your password click on the following link:

{{ url_for('auth.password_reset', token=token, _external=True) }}

If you have not requested a password reset simply ignore this message.

Sincerely,

The Flasky Team

Note: replies to this email address are not monitored.
1 change: 1 addition & 0 deletions app/templates/auth/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ <h1>Login</h1>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
<br>
<p>Forgot your password? <a href="{{ url_for('auth.password_reset_request') }}">Click here to reset it</a>.</p>
<p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p>
</div>
{% endblock %}
13 changes: 13 additions & 0 deletions app/templates/auth/reset_password.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Password Reset{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Reset Your Password</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
16 changes: 16 additions & 0 deletions tests/test_user_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,19 @@ def test_expired_confirmation_token(self):
token = u.generate_confirmation_token(1)
time.sleep(2)
self.assertFalse(u.confirm(token))

def test_valid_reset_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_reset_token()
self.assertTrue(User.reset_password(token, 'dog'))
self.assertTrue(u.verify_password('dog'))

def test_invalid_reset_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_reset_token()
self.assertFalse(User.reset_password(token + 'a', 'horse'))
self.assertTrue(u.verify_password('cat'))

0 comments on commit 4711144

Please sign in to comment.