Browse Source

Chapter 8: Email address changes (8h)

master
T. Meissner 6 years ago
parent
commit
492e1065f6
8 changed files with 124 additions and 1 deletions
  1. +11
    -0
      app/auth/forms.py
  2. +31
    -1
      app/auth/views.py
  3. +22
    -0
      app/models.py
  4. +13
    -0
      app/templates/auth/change_email.html
  5. +7
    -0
      app/templates/auth/email/change_email.html
  6. +11
    -0
      app/templates/auth/email/change_email.txt
  7. +1
    -0
      app/templates/base.html
  8. +28
    -0
      tests/test_user_model.py

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

@ -55,3 +55,14 @@ class PasswordResetForm(FlaskForm):
DataRequired(), EqualTo('password2', message='Passwords must match.')]) DataRequired(), EqualTo('password2', message='Passwords must match.')])
password2 = PasswordField('Confirm password', validators=[DataRequired()]) password2 = PasswordField('Confirm password', validators=[DataRequired()])
submit = SubmitField('Reset password') 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.')

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

@ -5,7 +5,7 @@ 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
PasswordResetRequestForm, PasswordResetForm, ChangeEmailForm
@auth.before_app_request @auth.before_app_request
@ -133,3 +133,33 @@ def password_reset(token):
else: else:
return redirect(url_for('main.index')) return redirect(url_for('main.index'))
return render_template('auth/reset_password.html', form=form) 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/<token>')
@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'))

+ 22
- 0
app/models.py View File

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


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

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Change Email Address{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Change Your Email Address</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}

+ 7
- 0
app/templates/auth/email/change_email.html View File

@ -0,0 +1,7 @@
<p>Dear {{ user.username }},</p>
<p>To confirm your new email address <a href="{{ url_for('auth.change_email', 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.change_email', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>

+ 11
- 0
app/templates/auth/email/change_email.txt View File

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

+ 1
- 0
app/templates/base.html View File

@ -31,6 +31,7 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Account <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Account <b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{{ url_for('auth.change_password') }}">Change Password</a></li> <li><a href="{{ url_for('auth.change_password') }}">Change Password</a></li>
<li><a href="{{ url_for('auth.change_email_request') }}">Change Email</a></li>
<li><a href="{{ url_for('auth.logout') }}">Log Out</a></li> <li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
</ul> </ul>
</li> </li>


+ 28
- 0
tests/test_user_model.py View File

@ -74,3 +74,31 @@ class UserModelTestCase(unittest.TestCase):
token = u.generate_reset_token() token = u.generate_reset_token()
self.assertFalse(User.reset_password(token+'a', 'horse')) self.assertFalse(User.reset_password(token+'a', 'horse'))
self.assertTrue(u.verify_password('cat')) 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')

Loading…
Cancel
Save