diff --git a/app/auth/forms.py b/app/auth/forms.py index c1bf9b7b2..d59738dc5 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('New 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.lower()).first(): + raise ValidationError('Email already registered.') diff --git a/app/auth/views.py b/app/auth/views.py index be0db842d..f4a8aa487 100644 --- a/app/auth/views.py +++ b/app/auth/views.py @@ -6,7 +6,7 @@ from ..models import User from ..email import send_email from .forms import LoginForm, RegistrationForm, ChangePasswordForm,\ - PasswordResetRequestForm, PasswordResetForm + PasswordResetRequestForm, PasswordResetForm, ChangeEmailForm @auth.before_app_request @@ -136,3 +136,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.lower() + 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 0584cf9ca..bfe4c6b50 100644 --- a/app/models.py +++ b/app/models.py @@ -69,6 +69,28 @@ def reset_password(token, new_password): 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: + 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 000000000..786b727a3 --- /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 %} \ No newline at end of file diff --git a/app/templates/auth/email/change_email.html b/app/templates/auth/email/change_email.html new file mode 100644 index 000000000..6d392a855 --- /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 000000000..d94902e10 --- /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 cd96c3338..1ab3e54cd 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -30,6 +30,7 @@ Account diff --git a/tests/test_user_model.py b/tests/test_user_model.py index 8436e49b8..201b3bac8 100644 --- a/tests/test_user_model.py +++ b/tests/test_user_model.py @@ -74,3 +74,31 @@ def test_invalid_reset_token(self): 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='john@example.com', password='cat') + db.session.add(u) + db.session.commit() + token = u.generate_email_change_token('susan@example.org') + self.assertTrue(u.change_email(token)) + self.assertTrue(u.email == 'susan@example.org') + + def test_invalid_email_change_token(self): + u1 = User(email='john@example.com', password='cat') + u2 = User(email='susan@example.org', password='dog') + db.session.add(u1) + db.session.add(u2) + db.session.commit() + token = u1.generate_email_change_token('david@example.net') + self.assertFalse(u2.change_email(token)) + self.assertTrue(u2.email == 'susan@example.org') + + def test_duplicate_email_change_token(self): + u1 = User(email='john@example.com', password='cat') + u2 = User(email='susan@example.org', password='dog') + db.session.add(u1) + db.session.add(u2) + db.session.commit() + token = u2.generate_email_change_token('john@example.com') + self.assertFalse(u2.change_email(token)) + self.assertTrue(u2.email == 'susan@example.org')