From 4711144641ef081daee3213210b166bab2e8a068 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 18 Jul 2017 07:55:23 -0700 Subject: [PATCH] Chapter 8: Password resets (8g) --- app/auth/forms.py | 13 +++++++ app/auth/views.py | 36 +++++++++++++++++++- app/models.py | 18 ++++++++++ app/templates/auth/email/reset_password.html | 8 +++++ app/templates/auth/email/reset_password.txt | 13 +++++++ app/templates/auth/login.html | 1 + app/templates/auth/reset_password.html | 13 +++++++ tests/test_user_model.py | 16 +++++++++ 8 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 app/templates/auth/email/reset_password.html create mode 100644 app/templates/auth/email/reset_password.txt create mode 100644 app/templates/auth/reset_password.html diff --git a/app/auth/forms.py b/app/auth/forms.py index 582e6a176..c1bf9b7b2 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -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') diff --git a/app/auth/views.py b/app/auth/views.py index 2f9cfd06c..be0db842d 100644 --- a/app/auth/views.py +++ b/app/auth/views.py @@ -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 @@ -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: + 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/', 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) diff --git a/app/models.py b/app/models.py index 6c6030e83..0584cf9ca 100644 --- a/app/models.py +++ b/app/models.py @@ -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 '' % self.username diff --git a/app/templates/auth/email/reset_password.html b/app/templates/auth/email/reset_password.html new file mode 100644 index 000000000..1eafdfe16 --- /dev/null +++ b/app/templates/auth/email/reset_password.html @@ -0,0 +1,8 @@ +

Dear {{ user.username }},

+

To reset your password click here.

+

Alternatively, you can paste the following link in your browser's address bar:

+

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

diff --git a/app/templates/auth/email/reset_password.txt b/app/templates/auth/email/reset_password.txt new file mode 100644 index 000000000..fc6826c07 --- /dev/null +++ b/app/templates/auth/email/reset_password.txt @@ -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. diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html index 1e14c7f5a..136a7539b 100644 --- a/app/templates/auth/login.html +++ b/app/templates/auth/login.html @@ -10,6 +10,7 @@

Login

{{ wtf.quick_form(form) }}
+

Forgot your password? Click here to reset it.

New user? Click here to register.

{% endblock %} diff --git a/app/templates/auth/reset_password.html b/app/templates/auth/reset_password.html new file mode 100644 index 000000000..995007744 --- /dev/null +++ b/app/templates/auth/reset_password.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}Flasky - Password Reset{% endblock %} + +{% block page_content %} + +
+ {{ wtf.quick_form(form) }} +
+{% endblock %} \ No newline at end of file diff --git a/tests/test_user_model.py b/tests/test_user_model.py index 4c8765774..8436e49b8 100644 --- a/tests/test_user_model.py +++ b/tests/test_user_model.py @@ -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'))