Skip to content

Commit d5b2e68

Browse files
Chapter 8: Email address changes (8h)
1 parent f10e11e commit d5b2e68

File tree

8 files changed

+124
-1
lines changed

8 files changed

+124
-1
lines changed

app/auth/forms.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,14 @@ class PasswordResetForm(FlaskForm):
5555
DataRequired(), EqualTo('password2', message='Passwords must match')])
5656
password2 = PasswordField('Confirm password', validators=[DataRequired()])
5757
submit = SubmitField('Reset Password')
58+
59+
60+
class ChangeEmailForm(FlaskForm):
61+
email = StringField('New Email', validators=[DataRequired(), Length(1, 64),
62+
Email()])
63+
password = PasswordField('Password', validators=[DataRequired()])
64+
submit = SubmitField('Update Email Address')
65+
66+
def validate_email(self, field):
67+
if User.query.filter_by(email=field.data.lower()).first():
68+
raise ValidationError('Email already registered.')

app/auth/views.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ..models import User
77
from ..email import send_email
88
from .forms import LoginForm, RegistrationForm, ChangePasswordForm,\
9-
PasswordResetRequestForm, PasswordResetForm
9+
PasswordResetRequestForm, PasswordResetForm, ChangeEmailForm
1010

1111

1212
@auth.before_app_request
@@ -136,3 +136,33 @@ def password_reset(token):
136136
else:
137137
return redirect(url_for('main.index'))
138138
return render_template('auth/reset_password.html', form=form)
139+
140+
141+
@auth.route('/change_email', methods=['GET', 'POST'])
142+
@login_required
143+
def change_email_request():
144+
form = ChangeEmailForm()
145+
if form.validate_on_submit():
146+
if current_user.verify_password(form.password.data):
147+
new_email = form.email.data.lower()
148+
token = current_user.generate_email_change_token(new_email)
149+
send_email(new_email, 'Confirm your email address',
150+
'auth/email/change_email',
151+
user=current_user, token=token)
152+
flash('An email with instructions to confirm your new email '
153+
'address has been sent to you.')
154+
return redirect(url_for('main.index'))
155+
else:
156+
flash('Invalid email or password.')
157+
return render_template("auth/change_email.html", form=form)
158+
159+
160+
@auth.route('/change_email/<token>')
161+
@login_required
162+
def change_email(token):
163+
if current_user.change_email(token):
164+
db.session.commit()
165+
flash('Your email address has been updated.')
166+
else:
167+
flash('Invalid request.')
168+
return redirect(url_for('main.index'))

app/models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,28 @@ def reset_password(token, new_password):
6969
db.session.add(user)
7070
return True
7171

72+
def generate_email_change_token(self, new_email, expiration=3600):
73+
s = Serializer(current_app.config['SECRET_KEY'], expiration)
74+
return s.dumps(
75+
{'change_email': self.id, 'new_email': new_email}).decode('utf-8')
76+
77+
def change_email(self, token):
78+
s = Serializer(current_app.config['SECRET_KEY'])
79+
try:
80+
data = s.loads(token.encode('utf-8'))
81+
except:
82+
return False
83+
if data.get('change_email') != self.id:
84+
return False
85+
new_email = data.get('new_email')
86+
if new_email is None:
87+
return False
88+
if self.query.filter_by(email=new_email).first() is not None:
89+
return False
90+
self.email = new_email
91+
db.session.add(self)
92+
return True
93+
7294
def __repr__(self):
7395
return '<User %r>' % self.username
7496

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% extends "base.html" %}
2+
{% import "bootstrap/wtf.html" as wtf %}
3+
4+
{% block title %}Flasky - Change Email Address{% endblock %}
5+
6+
{% block page_content %}
7+
<div class="page-header">
8+
<h1>Change Your Email Address</h1>
9+
</div>
10+
<div class="col-md-4">
11+
{{ wtf.quick_form(form) }}
12+
</div>
13+
{% endblock %}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<p>Dear {{ user.username }},</p>
2+
<p>To confirm your new email address <a href="{{ url_for('auth.change_email', token=token, _external=True) }}">click here</a>.</p>
3+
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
4+
<p>{{ url_for('auth.change_email', token=token, _external=True) }}</p>
5+
<p>Sincerely,</p>
6+
<p>The Flasky Team</p>
7+
<p><small>Note: replies to this email address are not monitored.</small></p>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Dear {{ user.username }},
2+
3+
To confirm your new email address click on the following link:
4+
5+
{{ url_for('auth.change_email', token=token, _external=True) }}
6+
7+
Sincerely,
8+
9+
The Flasky Team
10+
11+
Note: replies to this email address are not monitored.

app/templates/base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Account <b class="caret"></b></a>
3131
<ul class="dropdown-menu">
3232
<li><a href="{{ url_for('auth.change_password') }}">Change Password</a></li>
33+
<li><a href="{{ url_for('auth.change_email_request') }}">Change Email</a></li>
3334
<li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
3435
</ul>
3536
</li>

tests/test_user_model.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,31 @@ def test_invalid_reset_token(self):
7474
token = u.generate_reset_token()
7575
self.assertFalse(User.reset_password(token + 'a', 'horse'))
7676
self.assertTrue(u.verify_password('cat'))
77+
78+
def test_valid_email_change_token(self):
79+
u = User(email='[email protected]', password='cat')
80+
db.session.add(u)
81+
db.session.commit()
82+
token = u.generate_email_change_token('[email protected]')
83+
self.assertTrue(u.change_email(token))
84+
self.assertTrue(u.email == '[email protected]')
85+
86+
def test_invalid_email_change_token(self):
87+
u1 = User(email='[email protected]', password='cat')
88+
u2 = User(email='[email protected]', password='dog')
89+
db.session.add(u1)
90+
db.session.add(u2)
91+
db.session.commit()
92+
token = u1.generate_email_change_token('[email protected]')
93+
self.assertFalse(u2.change_email(token))
94+
self.assertTrue(u2.email == '[email protected]')
95+
96+
def test_duplicate_email_change_token(self):
97+
u1 = User(email='[email protected]', password='cat')
98+
u2 = User(email='[email protected]', password='dog')
99+
db.session.add(u1)
100+
db.session.add(u2)
101+
db.session.commit()
102+
token = u2.generate_email_change_token('[email protected]')
103+
self.assertFalse(u2.change_email(token))
104+
self.assertTrue(u2.email == '[email protected]')

0 commit comments

Comments
 (0)