Browse Source

Chapter 8: Account confirmation (8e)

master
T. Meissner 6 years ago
parent
commit
330e5fb933
7 changed files with 162 additions and 4 deletions
  1. +47
    -4
      app/auth/views.py
  2. +20
    -0
      app/models.py
  3. +8
    -0
      app/templates/auth/email/confirm.html
  4. +13
    -0
      app/templates/auth/email/confirm.txt
  5. +20
    -0
      app/templates/auth/unconfirmed.html
  6. +28
    -0
      migrations/versions/f8108bd4cd89_.py
  7. +26
    -0
      tests/test_user_model.py

+ 47
- 4
app/auth/views.py View File

@ -1,11 +1,28 @@
from flask import render_template, redirect, request, url_for, flash from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user, logout_user, login_required
from flask_login import login_user, logout_user, login_required, current_user
from . import auth from . import auth
from .. import db from .. import db
from ..models import User from ..models import User
from ..email import send_email
from .forms import LoginForm, RegistrationForm from .forms import LoginForm, RegistrationForm
@auth.before_app_request
def before_request():
if current_user.is_authenticated \
and not current_user.confirmed \
and request.blueprint != 'auth' \
and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous or current_user.confirmed:
return redirect(url_for('main.index'))
return render_template('auth/unconfirmed.html')
@auth.route('/login', methods=['GET', 'POST']) @auth.route('/login', methods=['GET', 'POST'])
def login(): def login():
form = LoginForm() form = LoginForm()
@ -38,6 +55,32 @@ def register():
password=form.password.data) password=form.password.data)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
flash('You can now login.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
token = user.generate_confirmation_token()
send_email(user.email, 'Confirm your account',
'auth/email/confirm', user=user, token=token)
flash('A confirmation email has been sent to you by email.')
return redirect(url_for('main.index'))
return render_template('auth/register.html', form=form)
@auth.route('/confirm/<token>')
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('main.index'))
if current_user.confirm(token):
db.session.commit()
flash('You have confirmed your account. Thanks!')
else:
flash('The confirmationlink is invalid or has expired')
return redirect(url_for('main.index'))
@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email(current_user.email, 'Confirm your account',
'auth/email/confirm', user=current_user, token=token)
flash('A new confirmation email has been sent to you by email.')
return redirect(url_for('main.index'))

+ 20
- 0
app/models.py View File

@ -1,4 +1,7 @@
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from itsdangerous import BadSignature
from flask import current_app
from flask_login import UserMixin from flask_login import UserMixin
from . import db, login_manager from . import db, login_manager
@ -20,6 +23,7 @@ class User(UserMixin, db.Model):
username = db.Column(db.String(64), unique=True, index=True) username = db.Column(db.String(64), unique=True, index=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
password_hash = db.Column(db.String(128)) password_hash = db.Column(db.String(128))
confirmed = db.Column(db.Boolean, default=False)
@property @property
def password(self): def password(self):
@ -32,6 +36,22 @@ class User(UserMixin, db.Model):
def verify_password(self, password): def verify_password(self, password):
return check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
def generate_confirmation_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id}).decode('utf-8')
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except BadSignature:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
return True
def __repr__(self): def __repr__(self):
return '<User %r>' % self.username return '<User %r>' % self.username


+ 8
- 0
app/templates/auth/email/confirm.html View File

@ -0,0 +1,8 @@
<p>Dear {{ user.username }},</p>
<p>Welcome to <b>Flasky</b>!</p>
<p>To confirm your account please <a href="{{ url_for('auth.confirm', 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.confirm', 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>

+ 13
- 0
app/templates/auth/email/confirm.txt View File

@ -0,0 +1,13 @@
Dear {{ user.username }},
Welcome to Flasky!
To confirm your account please klick on the following link:
{{ url_for('auth.confirm', token=token, _external=True) }}
Sincerely,
The Flasky Team
Note: replies to this email address are not monitored.

+ 20
- 0
app/templates/auth/unconfirmed.html View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}Flasky - Confirm your account{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>
Hello, {{ current_user.username }}!
</h1>
<h3>You have not confirmed your account yet.</h3>
<p>
Before you can access this site you need to confirm your account.
Check your inbox, you should have received an email with a confirmation link.
</p>
<p>
Need another confirmation email?
<a href="{{ url_for('auth.resend_confirmation') }}">Click here</a>
</p>
</div>
{% endblock %}

+ 28
- 0
migrations/versions/f8108bd4cd89_.py View File

@ -0,0 +1,28 @@
"""empty message
Revision ID: f8108bd4cd89
Revises: 655013143dbb
Create Date: 2018-11-04 23:53:16.391929
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f8108bd4cd89'
down_revision = '655013143dbb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('confirmed', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'confirmed')
# ### end Alembic commands ###

+ 26
- 0
tests/test_user_model.py View File

@ -1,4 +1,5 @@
import unittest import unittest
import time
from app import create_app, db from app import create_app, db
from app.models import User from app.models import User
@ -33,3 +34,28 @@ class UserModelTestCase(unittest.TestCase):
u = User(password='cat') u = User(password='cat')
u2 = User(password='cat') u2 = User(password='cat')
self.assertTrue(u.password_hash != u2.password_hash) self.assertTrue(u.password_hash != u2.password_hash)
def test_valid_confirmation_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_confirmation_token()
self.assertTrue(u.confirm(token))
def test_invalid_confirmation_token(self):
u1 = User(password='cat')
u2 = User(password='dog')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token = u1.generate_confirmation_token()
self.assertFalse(u2.confirm(token))
def test_expired_confirmation_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_confirmation_token(1)
time.sleep(2)
self.assertFalse(u.confirm(token))

Loading…
Cancel
Save