Added register signal, some testing utils and basic confirmation

This commit is contained in:
Matt Wright
2012-05-11 18:15:46 -04:00
parent 1a8e6f5cb6
commit 4c1a16e2ee
10 changed files with 161 additions and 17 deletions
+2 -1
View File
@@ -93,7 +93,7 @@ def create_app(auth_config):
def create_sqlalchemy_app(auth_config=None):
app = create_app(auth_config)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/flask_security_example.sqlite'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
db = SQLAlchemy(app)
@@ -113,6 +113,7 @@ def create_sqlalchemy_app(auth_config=None):
active = db.Column(db.Boolean())
confirmation_token = db.Column(db.String(255))
confirmation_sent_at = db.Column(db.DateTime())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
+28 -2
View File
@@ -2,7 +2,8 @@
from datetime import datetime
from flask import current_app, request, url_for
from flask.ext.security.exceptions import UserNotFoundError
from flask.ext.security.exceptions import UserNotFoundError, \
ConfirmationError, ConfirmationExpiredError
from flask.ext.security.utils import generate_token, send_mail
from werkzeug.local import LocalProxy
@@ -10,6 +11,12 @@ security = LocalProxy(lambda: current_app.security)
logger = LocalProxy(lambda: current_app.logger)
def find_user_by_confirmation_token(token):
if not token:
raise ConfirmationError('Unknown confirmation token')
return security.datastore.find_user(confirmation_token=token)
def send_confirmation_instructions(user):
url = url_for('flask_security.confirm',
confirmation_token=user.confirmation_token)
@@ -20,12 +27,14 @@ def send_confirmation_instructions(user):
'confirmation_instructions',
dict(user=user, confirmation_link=confirmation_link))
return True
def generate_confirmation_token(user):
while True:
token = generate_token()
try:
security.datastore.find_user(confirmation_token=token)
find_user_by_confirmation_token(token)
except UserNotFoundError:
break
@@ -39,3 +48,20 @@ def generate_confirmation_token(user):
user.confirmation_sent_at = now
return user
def confirm_by_token(token):
now = datetime.utcnow()
user = find_user_by_confirmation_token(token)
token_expires = now - security.confirm_email_within
if user.confirmation_sent_at < token_expires:
raise ConfirmationExpiredError('Confirmation token is expired', user=user)
user.confirmed_at = now
user.confirmation_token = None
user.confirmation_sent_at = None
security.datastore._save_model(user)
return user
+4 -1
View File
@@ -42,6 +42,7 @@ _default_config = {
'SECURITY_POST_LOGIN_VIEW': '/',
'SECURITY_POST_LOGOUT_VIEW': '/',
'SECURITY_POST_REGISTER_VIEW': '/',
'SECURITY_POST_CONFIRM_VIEW': '/',
'SECURITY_RESET_PASSWORD_WITHIN': 10,
'SECURITY_DEFAULT_ROLES': [],
'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True,
@@ -234,13 +235,15 @@ class Security(object):
self.post_login_view = utils.config_value(app, 'POST_LOGIN_VIEW')
self.post_logout_view = utils.config_value(app, 'POST_LOGOUT_VIEW')
self.post_register_view = utils.config_value(app, 'POST_REGISTER_VIEW')
self.post_confirm_view = utils.config_value(app, 'POST_CONFIRM_VIEW')
self.reset_password_within = utils.config_value(app, 'RESET_PASSWORD_WITHIN')
self.default_roles = utils.config_value(app, "DEFAULT_ROLES")
self.login_without_confirmation = utils.config_value(app, 'LOGIN_WITHOUT_CONFIRMATION')
self.confirm_email = utils.config_value(app, 'CONFIRM_EMAIL')
self.email_sender = utils.config_value(app, 'EMAIL_SENDER')
self.confirm_email_within_text = utils.config_value(app, 'CONFIRM_EMAIL_WITHIN')
values = utils.config_value(app, 'CONFIRM_EMAIL_WITHIN').split()
values = self.confirm_email_within_text.split()
self.confirm_email_within = timedelta(**{values[1]: int(values[0])})
identity_loaded.connect_via(app)(on_identity_loaded)
+1
View File
@@ -91,6 +91,7 @@ class UserDatastore(object):
:param user: User identifier, usually email address
"""
print kwargs
user = self._do_find_user(**kwargs)
if user:
return user
+14
View File
@@ -53,3 +53,17 @@ class UserCreationError(Exception):
class RoleCreationError(Exception):
"""Raised when an error occurs when creating a role
"""
class ConfirmationError(Exception):
"""Raised when an unknown confirmation error occurs
"""
class ConfirmationExpiredError(Exception):
"""Raised when a user attempts to confirm their email but their token
has expired
"""
def __init__(self, msg, user=None):
super(ConfirmationExpiredError, self).__init__(msg)
self.user = user
+5
View File
@@ -0,0 +1,5 @@
import blinker
signals = blinker.Namespace()
user_registered = signals.signal("user-register")
+21
View File
@@ -12,9 +12,11 @@
import base64
import os
from contextlib import contextmanager
from importlib import import_module
from flask import url_for, flash, current_app, request, session, render_template
from flask.ext.security.signals import user_registered
def generate_token():
@@ -76,3 +78,22 @@ def send_mail(subject, recipient, template, context):
msg.html = render_template('email/%s.html' % template, **context)
current_app.mail.send(msg)
@contextmanager
def capture_registrations(confirmation_sent_at=None):
users = []
def _on(user, app):
if confirmation_sent_at:
user.confirmation_sent_at = confirmation_sent_at
current_app.security.datastore._save_model(user)
users.append(user)
user_registered.connect(_on)
try:
yield users
finally:
user_registered.disconnect(_on)
+31 -3
View File
@@ -12,11 +12,12 @@
from flask import current_app, redirect, request, session
from flask.ext.login import login_user, logout_user
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
from flask.ext.security import exceptions, utils, confirmable
from flask.ext.security import exceptions, utils, confirmable, signals
from werkzeug.local import LocalProxy
security = LocalProxy(lambda: current_app.security)
logger = LocalProxy(lambda: current_app.logger)
@@ -77,6 +78,9 @@ def register():
user = security.datastore.create_user(**params)
app = current_app._get_current_object()
signals.user_registered.send(user, app=app)
if security.confirm_email:
confirmable.send_confirmation_instructions(user)
@@ -84,14 +88,38 @@ def register():
do_login(user)
url = security.post_register_view
logger.debug("User %s registered. Redirect to: %s" % (user, url))
logger.debug('User %s registered. Redirect to: %s' % (user, url))
return redirect(url)
return redirect(request.referrer or security.register_url)
def confirm():
token = request.args.get('confirmation_token', None)
try:
token = request.args.get('confirmation_token', None)
user = confirmable.confirm_by_token(token)
except exceptions.ConfirmationError, e:
utils.do_flash(str(e), 'error')
return redirect('/') # TODO: Don't just redirect to root
except exceptions.ConfirmationExpiredError, e:
user = e.user
confirmable.generate_confirmation_token(user)
confirmable.send_confirmation_instructions(user)
msg = 'You did not confirm your email within %s. ' \
'A new confirmation code has been sent to %s' % (
security.confirm_email_within_text, user.email)
utils.do_flash(msg, 'error')
return redirect('/')
do_login(user)
utils.do_flash('Thank you! Your email has been confirmed', 'success')
return redirect(security.post_confirm_view or security.post_login_view)
def reset():
View File
+55 -10
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
import unittest
from datetime import datetime, timedelta
from flask.ext.security.utils import capture_registrations
from example import app
@@ -149,6 +152,58 @@ class ConfiguredURLTests(SecurityTest):
class ConfirmationTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRM_EMAIL': True
}
def register(self, email, password='password'):
data = dict(email=email, password=password, password_confirm=password)
return self.client.post('/register', data=data, follow_redirects=True)
def test_register_sends_confirmation_email(self):
e = 'dude@lp.com'
with self.app.mail.record_messages() as outbox:
self.register(e)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
def test_confirm_email(self):
e = 'dude@lp.com'
with capture_registrations() as users:
self.register(e)
token = users[0].confirmation_token
r = self.client.get('/confirm?confirmation_token=' + token, follow_redirects=True)
self.assertIn('Thank you! Your email has been confirmed', r.data)
def test_invalid_or_unprovided_token_when_confirming_email(self):
r = self.client.get('/confirm', follow_redirects=True)
self.assertIn('Unknown confirmation token', r.data)
def test_expired_confirmation_token_sends_email(self):
e = 'dude@lp.com'
sent_at = datetime.utcnow() - timedelta(days=15)
with capture_registrations(confirmation_sent_at=sent_at) as users:
self.register(e)
token = users[0].confirmation_token
with self.app.mail.record_messages() as outbox:
r = self.client.get('/confirm?confirmation_token=' + token, follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
self.assertNotIn(token, outbox[0].html)
expire_text = self.app.security.confirm_email_within_text
text = 'You did not confirm your email within %s' % expire_text
self.assertIn(text, r.data)
class ConfirmationAndCanLoginTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRM_EMAIL': True,
'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True
@@ -161,16 +216,6 @@ class ConfirmationTests(SecurityTest):
r = self.client.post('/register', data=data, follow_redirects=True)
self.assertIn(e, r.data)
def test_register_valid_user_sends_confirmation_email(self):
e = 'dude@lp.com'
p = 'password'
data = dict(email=e, password=p, password_confirm=p)
with self.app.mail.record_messages() as outbox:
self.client.post('/register', data=data, follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
class MongoEngineSecurityTests(DefaultSecurityTests):