From 492e1065f63f6efea380347a7603c1ff7ba2ac46 Mon Sep 17 00:00:00 2001 From: tmeissner Date: Tue, 6 Nov 2018 22:47:03 +0100 Subject: [PATCH] Chapter 8: Email address changes (8h) --- app/auth/forms.py | 11 ++++++++ app/auth/views.py | 32 +++++++++++++++++++++- app/models.py | 22 +++++++++++++++ app/templates/auth/change_email.html | 13 +++++++++ app/templates/auth/email/change_email.html | 7 +++++ app/templates/auth/email/change_email.txt | 11 ++++++++ app/templates/base.html | 1 + tests/test_user_model.py | 28 +++++++++++++++++++ 8 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 app/templates/auth/change_email.html create mode 100644 app/templates/auth/email/change_email.html create mode 100644 app/templates/auth/email/change_email.txt diff --git a/app/auth/forms.py b/app/auth/forms.py index 17ca675..3544178 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -55,3 +55,14 @@ class PasswordResetForm(FlaskForm): DataRequired(), EqualTo('password2', message='Passwords must match.')]) password2 = PasswordField('Confirm password', validators=[DataRequired()]) submit = SubmitField('Reset password') + + +class ChangeEmailForm(FlaskForm): + email = StringField('Email', validators=[DataRequired(), Length(1, 64, + Email())]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Update Email address') + + def validate_email(self, field): + if User.query.filter_by(email=field.data).first(): + raise ValidationError('Email already registered.') diff --git a/app/auth/views.py b/app/auth/views.py index fbc90d4..a14c826 100644 --- a/app/auth/views.py +++ b/app/auth/views.py @@ -5,7 +5,7 @@ from .. import db from ..models import User from ..email import send_email from .forms import LoginForm, RegistrationForm, ChangePasswordForm, \ - PasswordResetRequestForm, PasswordResetForm + PasswordResetRequestForm, PasswordResetForm, ChangeEmailForm @auth.before_app_request @@ -133,3 +133,33 @@ def password_reset(token): else: return redirect(url_for('main.index')) return render_template('auth/reset_password.html', form=form) + + +@auth.route('/change-email', methods=['GET', 'POST']) +@login_required +def change_email_request(): + form = ChangeEmailForm() + if form.validate_on_submit(): + if current_user.verify_password(form.password.data): + new_email = form.email.data + token = current_user.generate_email_change_token(new_email) + send_email(new_email, 'Confirm your email address', + 'auth/email/change_email', + user=current_user, token=token) + flash('An email with instructions to confirm your new email ' + 'address has been sent to you') + return redirect(url_for('main.index')) + else: + flash('Invalid email or password') + return render_template('auth/change_email.html', form=form) + + +@auth.route('/change-email/') +@login_required +def change_email(token): + if current_user.change_email(token): + db.session.commit() + flash('Your email address has been updated.') + else: + flash('Invalid request.') + return redirect(url_for('main.index')) diff --git a/app/models.py b/app/models.py index 956d293..c52a82a 100644 --- a/app/models.py +++ b/app/models.py @@ -70,6 +70,28 @@ class User(UserMixin, db.Model): db.session.add(user) return True + def generate_email_change_token(self, new_email, expiration=3600): + s = Serializer(current_app.config['SECRET_KEY'], expiration) + return s.dumps( + {'change_email': self.id, 'new_email': new_email}).decode('utf-8') + + def change_email(self, token): + s = Serializer(current_app.config['SECRET_KEY']) + try: + data = s.loads(token.encode('utf-8')) + except BadSignature: + return False + if data.get('change_email') != self.id: + return False + new_email = data.get('new_email') + if new_email is None: + return False + if self.query.filter_by(email=new_email).first() is not None: + return False + self.email = new_email + db.session.add(self) + return True + def __repr__(self): return '' % self.username diff --git a/app/templates/auth/change_email.html b/app/templates/auth/change_email.html new file mode 100644 index 0000000..bbc6062 --- /dev/null +++ b/app/templates/auth/change_email.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}Flasky - Change Email Address{% endblock %} + +{% block page_content %} + +
+ {{ wtf.quick_form(form) }} +
+{% endblock %} diff --git a/app/templates/auth/email/change_email.html b/app/templates/auth/email/change_email.html new file mode 100644 index 0000000..6d392a8 --- /dev/null +++ b/app/templates/auth/email/change_email.html @@ -0,0 +1,7 @@ +

Dear {{ user.username }},

+

To confirm your new email address click here.

+

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

+

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

+

Sincerely,

+

The Flasky Team

+

Note: replies to this email address are not monitored.

diff --git a/app/templates/auth/email/change_email.txt b/app/templates/auth/email/change_email.txt new file mode 100644 index 0000000..d94902e --- /dev/null +++ b/app/templates/auth/email/change_email.txt @@ -0,0 +1,11 @@ +Dear {{ user.username }}, + +To confirm your new email address click on the following link: + +{{ url_for('auth.change_email', token=token, _external=True) }} + +Sincerely, + +The Flasky Team + +Note: replies to this email address are not monitored. diff --git a/app/templates/base.html b/app/templates/base.html index 7e58752..c552aec 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -31,6 +31,7 @@ Account diff --git a/tests/test_user_model.py b/tests/test_user_model.py index 7c1c0cc..3d16c87 100644 --- a/tests/test_user_model.py +++ b/tests/test_user_model.py @@ -74,3 +74,31 @@ class UserModelTestCase(unittest.TestCase): token = u.generate_reset_token() self.assertFalse(User.reset_password(token+'a', 'horse')) self.assertTrue(u.verify_password('cat')) + + def test_valid_email_change_token(self): + u = User(email='max@mustermann.de', password='cat') + db.session.add(u) + db.session.commit() + token = u.generate_email_change_token('foo@bar.de') + self.assertTrue(u.change_email(token)) + self.assertTrue(u.email == 'foo@bar.de') + + def test_invalid_email_change_token(self): + u1 = User(email='max@mustermann.de', password='cat') + u2 = User(email='dirk@mustermann.de', password='dog') + db.session.add(u1) + db.session.add(u2) + db.session.commit() + token = u1.generate_email_change_token('foo@bar.de') + self.assertFalse(u2.change_email(token)) + self.assertTrue(u2.email == 'dirk@mustermann.de') + + def test_duplicate_email_change_token(self): + u1 = User(email='max@mustermann.de', password='cat') + u2 = User(email='dirk@mustermann.de', password='dog') + db.session.add(u1) + db.session.add(u2) + db.session.commit() + token = u2.generate_email_change_token(u1.email) + self.assertFalse(u2.change_email(token)) + self.assertTrue(u2.email == 'dirk@mustermann.de')