Skip to content

Commit edb7ecb

Browse files
Chapter 8: Account confirmation (8e)
1 parent 0169aea commit edb7ecb

8 files changed

Lines changed: 156 additions & 2 deletions

File tree

app/auth/views.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@
88
from .forms import LoginForm, RegistrationForm
99

1010

11+
@auth.before_app_request
12+
def before_request():
13+
if current_user.is_authenticated \
14+
and not current_user.confirmed \
15+
and request.endpoint \
16+
and request.blueprint != 'auth' \
17+
and request.endpoint != 'static':
18+
return redirect(url_for('auth.unconfirmed'))
19+
20+
21+
@auth.route('/unconfirmed')
22+
def unconfirmed():
23+
if current_user.is_anonymous or current_user.confirmed:
24+
return redirect(url_for('main.index'))
25+
return render_template('auth/unconfirmed.html')
26+
27+
1128
@auth.route('/login', methods=['GET', 'POST'])
1229
def login():
1330
form = LoginForm()
@@ -40,6 +57,32 @@ def register():
4057
password=form.password.data)
4158
db.session.add(user)
4259
db.session.commit()
43-
flash('You can now login.')
60+
token = user.generate_confirmation_token()
61+
send_email(user.email, 'Confirm Your Account',
62+
'auth/email/confirm', user=user, token=token)
63+
flash('A confirmation email has been sent to you by email.')
4464
return redirect(url_for('auth.login'))
4565
return render_template('auth/register.html', form=form)
66+
67+
68+
@auth.route('/confirm/<token>')
69+
@login_required
70+
def confirm(token):
71+
if current_user.confirmed:
72+
return redirect(url_for('main.index'))
73+
if current_user.confirm(token):
74+
db.session.commit()
75+
flash('You have confirmed your account. Thanks!')
76+
else:
77+
flash('The confirmation link is invalid or has expired.')
78+
return redirect(url_for('main.index'))
79+
80+
81+
@auth.route('/confirm')
82+
@login_required
83+
def resend_confirmation():
84+
token = current_user.generate_confirmation_token()
85+
send_email(current_user.email, 'Confirm Your Account',
86+
'auth/email/confirm', user=current_user, token=token)
87+
flash('A new confirmation email has been sent to you by email.')
88+
return redirect(url_for('main.index'))

app/models.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from werkzeug.security import generate_password_hash, check_password_hash
2+
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
3+
from flask import current_app
24
from flask_login import UserMixin
35
from . import db, login_manager
46

@@ -20,6 +22,7 @@ class User(UserMixin, db.Model):
2022
username = db.Column(db.String(64), unique=True, index=True)
2123
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
2224
password_hash = db.Column(db.String(128))
25+
confirmed = db.Column(db.Boolean, default=False)
2326

2427
@property
2528
def password(self):
@@ -32,6 +35,22 @@ def password(self, password):
3235
def verify_password(self, password):
3336
return check_password_hash(self.password_hash, password)
3437

38+
def generate_confirmation_token(self, expiration=3600):
39+
s = Serializer(current_app.config['SECRET_KEY'], expiration)
40+
return s.dumps({'confirm': self.id}).decode('utf-8')
41+
42+
def confirm(self, token):
43+
s = Serializer(current_app.config['SECRET_KEY'])
44+
try:
45+
data = s.loads(token.encode('utf-8'))
46+
except:
47+
return False
48+
if data.get('confirm') != self.id:
49+
return False
50+
self.confirmed = True
51+
db.session.add(self)
52+
return True
53+
3554
def __repr__(self):
3655
return '<User %r>' % self.username
3756

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<p>Dear {{ user.username }},</p>
2+
<p>Welcome to <b>Flasky</b>!</p>
3+
<p>To confirm your account please <a href="{{ url_for('auth.confirm', token=token, _external=True) }}">click here</a>.</p>
4+
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
5+
<p>{{ url_for('auth.confirm', token=token, _external=True) }}</p>
6+
<p>Sincerely,</p>
7+
<p>The Flasky Team</p>
8+
<p><small>Note: replies to this email address are not monitored.</small></p>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Dear {{ user.username }},
2+
3+
Welcome to Flasky!
4+
5+
To confirm your account please click on the following link:
6+
7+
{{ url_for('auth.confirm', token=token, _external=True) }}
8+
9+
Sincerely,
10+
11+
The Flasky Team
12+
13+
Note: replies to this email address are not monitored.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{% extends "base.html" %}
2+
3+
{% block title %}Flasky - Confirm your account{% endblock %}
4+
5+
{% block page_content %}
6+
<div class="page-header">
7+
<h1>
8+
Hello, {{ current_user.username }}!
9+
</h1>
10+
<h3>You have not confirmed your account yet.</h3>
11+
<p>
12+
Before you can access this site you need to confirm your account.
13+
Check your inbox, you should have received an email with a confirmation link.
14+
</p>
15+
<p>
16+
Need another confirmation email?
17+
<a href="{{ url_for('auth.resend_confirmation') }}">Click here</a>
18+
</p>
19+
</div>
20+
{% endblock %}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""account confirmation
2+
3+
Revision ID: 190163627111
4+
Revises: 456a945560f6
5+
Create Date: 2013-12-29 02:58:45.577428
6+
7+
"""
8+
9+
# revision identifiers, used by Alembic.
10+
revision = '190163627111'
11+
down_revision = '456a945560f6'
12+
13+
from alembic import op
14+
import sqlalchemy as sa
15+
16+
17+
def upgrade():
18+
### commands auto generated by Alembic - please adjust! ###
19+
op.add_column('users', sa.Column('confirmed', sa.Boolean(), nullable=True))
20+
### end Alembic commands ###
21+
22+
23+
def downgrade():
24+
### commands auto generated by Alembic - please adjust! ###
25+
op.drop_column('users', 'confirmed')
26+
### end Alembic commands ###

migrations/versions/456a945560f6_login_support.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ def downgrade():
2727
op.drop_index('ix_users_email', 'users')
2828
op.drop_column('users', 'password_hash')
2929
op.drop_column('users', 'email')
30-
### end Alembic commands ###
30+
### end Alembic commands ###

tests/test_user_model.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import unittest
2+
import time
23
from app import create_app, db
34
from app.models import User
45

@@ -33,3 +34,27 @@ def test_password_salts_are_random(self):
3334
u = User(password='cat')
3435
u2 = User(password='cat')
3536
self.assertTrue(u.password_hash != u2.password_hash)
37+
38+
def test_valid_confirmation_token(self):
39+
u = User(password='cat')
40+
db.session.add(u)
41+
db.session.commit()
42+
token = u.generate_confirmation_token()
43+
self.assertTrue(u.confirm(token))
44+
45+
def test_invalid_confirmation_token(self):
46+
u1 = User(password='cat')
47+
u2 = User(password='dog')
48+
db.session.add(u1)
49+
db.session.add(u2)
50+
db.session.commit()
51+
token = u1.generate_confirmation_token()
52+
self.assertFalse(u2.confirm(token))
53+
54+
def test_expired_confirmation_token(self):
55+
u = User(password='cat')
56+
db.session.add(u)
57+
db.session.commit()
58+
token = u.generate_confirmation_token(1)
59+
time.sleep(2)
60+
self.assertFalse(u.confirm(token))

0 commit comments

Comments
 (0)