Browse Source

Chapter 8: Password resets (8g)

master
T. Meissner 6 years ago
parent
commit
3571a77933
8 changed files with 117 additions and 7 deletions
  1. +13
    -0
      app/auth/forms.py
  2. +34
    -1
      app/auth/views.py
  3. +18
    -0
      app/models.py
  4. +8
    -0
      app/templates/auth/email/reset_password.html
  5. +13
    -0
      app/templates/auth/email/reset_password.txt
  6. +2
    -6
      app/templates/auth/login.html
  7. +13
    -0
      app/templates/auth/reset_password.html
  8. +16
    -0
      tests/test_user_model.py

+ 13
- 0
app/auth/forms.py View File

@ -42,3 +42,16 @@ class ChangePasswordForm(FlaskForm):
password2 = PasswordField('Confirm new password', password2 = PasswordField('Confirm new password',
validators=[DataRequired()]) validators=[DataRequired()])
submit = SubmitField('Update password') 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')

+ 34
- 1
app/auth/views.py View File

@ -4,7 +4,8 @@ from . import auth
from .. import db from .. import db
from ..models import User from ..models import User
from ..email import send_email from ..email import send_email
from .forms import LoginForm, RegistrationForm, ChangePasswordForm
from .forms import LoginForm, RegistrationForm, ChangePasswordForm, \
PasswordResetRequestForm, PasswordResetForm
@auth.before_app_request @auth.before_app_request
@ -100,3 +101,35 @@ def change_password():
else: else:
flash('Invalid password.') flash('Invalid password.')
return render_template('auth/change_password.html', form=form) 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:
redirect(url_for('main.index'))
form = PasswordResetRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).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:
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
- 0
app/models.py View File

@ -52,6 +52,24 @@ class User(UserMixin, db.Model):
db.session.add(self) db.session.add(self)
return True 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 BadSignature:
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): def __repr__(self):
return '<User %r>' % self.username return '<User %r>' % self.username


+ 8
- 0
app/templates/auth/email/reset_password.html View File

@ -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
- 0
app/templates/auth/email/reset_password.txt View File

@ -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.

+ 2
- 6
app/templates/auth/login.html View File

@ -10,11 +10,7 @@
<div class="col-md-4"> <div class="col-md-4">
{{ wtf.quick_form(form) }} {{ wtf.quick_form(form) }}
<br> <br>
<p>
New user?
<a href="{{ url_for('auth.register') }}">
Click here to register
</a>
</p>
<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> </div>
{% endblock %} {% endblock %}

+ 13
- 0
app/templates/auth/reset_password.html View File

@ -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
- 0
tests/test_user_model.py View File

@ -58,3 +58,19 @@ class UserModelTestCase(unittest.TestCase):
token = u.generate_confirmation_token(1) token = u.generate_confirmation_token(1)
time.sleep(2) time.sleep(2)
self.assertFalse(u.confirm(token)) 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'))

Loading…
Cancel
Save