mirror of
https://github.com/wassname/flask-security.git
synced 2026-07-26 13:18:38 +08:00
Added register signal, some testing utils and basic confirmation
This commit is contained in:
+2
-1
@@ -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'))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import blinker
|
||||
|
||||
signals = blinker.Namespace()
|
||||
|
||||
user_registered = signals.signal("user-register")
|
||||
@@ -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
@@ -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():
|
||||
|
||||
+55
-10
@@ -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):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user