mirror of
https://github.com/wassname/flask-security.git
synced 2026-07-01 16:50:07 +08:00
Add trackable option and make extra features default to off/False to minimize about of application setup to get started
This commit is contained in:
@@ -129,6 +129,12 @@ def create_sqlalchemy_app(auth_config=None):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(255), unique=True)
|
||||
password = db.Column(db.String(120))
|
||||
remember_token = db.Column(db.String(255))
|
||||
last_login_at = db.Column(db.DateTime())
|
||||
current_login_at = db.Column(db.DateTime())
|
||||
last_login_ip = db.Column(db.String(100))
|
||||
current_login_ip = db.Column(db.String(100))
|
||||
login_count = db.Column(db.Integer)
|
||||
active = db.Column(db.Boolean())
|
||||
confirmation_token = db.Column(db.String(255))
|
||||
confirmation_sent_at = db.Column(db.DateTime())
|
||||
@@ -166,6 +172,12 @@ def create_mongoengine_app(auth_config=None):
|
||||
class User(db.Document, UserMixin):
|
||||
email = db.StringField(unique=True, max_length=255)
|
||||
password = db.StringField(required=True, max_length=120)
|
||||
remember_token = db.StringField(max_length=255)
|
||||
last_login_at = db.DateTimeField()
|
||||
current_login_at = db.DateTimeField()
|
||||
last_login_ip = db.StringField(max_length=100)
|
||||
current_login_ip = db.StringField(max_length=100)
|
||||
login_count = db.IntField()
|
||||
active = db.BooleanField(default=True)
|
||||
confirmation_token = db.StringField(max_length=255)
|
||||
confirmation_sent_at = db.DateTimeField()
|
||||
|
||||
+19
-5
@@ -22,7 +22,6 @@ from . import views, exceptions, utils
|
||||
from .confirmable import confirmation_token_is_expired, requires_confirmation, \
|
||||
reset_confirmation_token
|
||||
from .decorators import login_required
|
||||
from .forms import Form, LoginForm
|
||||
|
||||
|
||||
#: Default Flask-Security configuration
|
||||
@@ -50,8 +49,9 @@ _default_config = {
|
||||
'POST_CONFIRM_VIEW': None,
|
||||
'DEFAULT_ROLES': [],
|
||||
'CONFIRMABLE': False,
|
||||
'REGISTERABLE': True,
|
||||
'RECOVERABLE': True,
|
||||
'REGISTERABLE': False,
|
||||
'RECOVERABLE': False,
|
||||
'TRACKABLE': False,
|
||||
'CONFIRM_EMAIL_WITHIN': '5 days',
|
||||
'RESET_PASSWORD_WITHIN': '2 days',
|
||||
'LOGIN_WITHOUT_CONFIRMATION': False,
|
||||
@@ -80,6 +80,10 @@ class UserMixin(BaseUserMixin):
|
||||
"""Returns `True` if the user is active."""
|
||||
return self.active
|
||||
|
||||
def get_auth_token(self):
|
||||
"""Returns the user's authentication token."""
|
||||
self.remember_token
|
||||
|
||||
def has_role(self, role):
|
||||
"""Returns `True` if the user identifies with the specified role.
|
||||
|
||||
@@ -103,7 +107,7 @@ class AnonymousUser(AnonymousUserBase):
|
||||
return False
|
||||
|
||||
|
||||
def _load_user(user_id):
|
||||
def _user_loader(user_id):
|
||||
try:
|
||||
return current_app.security.datastore.with_id(user_id)
|
||||
except Exception, e:
|
||||
@@ -111,6 +115,14 @@ def _load_user(user_id):
|
||||
return None
|
||||
|
||||
|
||||
def _token_loader(token):
|
||||
try:
|
||||
return current_app.security.datastore.find_user(remember_token=token)
|
||||
except Exception, e:
|
||||
current_app.logger.error('Error getting user: %s' % e)
|
||||
return None
|
||||
|
||||
|
||||
def _on_identity_loaded(sender, identity):
|
||||
if hasattr(current_user, 'id'):
|
||||
identity.provides.add(UserNeed(current_user.id))
|
||||
@@ -150,7 +162,8 @@ class Security(object):
|
||||
login_manager = LoginManager()
|
||||
login_manager.anonymous_user = AnonymousUser
|
||||
login_manager.login_view = utils.config_value(app, 'LOGIN_VIEW')
|
||||
login_manager.user_loader(_load_user)
|
||||
login_manager.user_loader(_user_loader)
|
||||
login_manager.token_loader(_token_loader)
|
||||
login_manager.init_app(app)
|
||||
|
||||
Provider = utils.get_class_from_string(app, 'AUTH_PROVIDER')
|
||||
@@ -182,6 +195,7 @@ class Security(object):
|
||||
self.confirmable = utils.config_value(app, 'CONFIRMABLE')
|
||||
self.registerable = utils.config_value(app, 'REGISTERABLE')
|
||||
self.recoverable = utils.config_value(app, 'RECOVERABLE')
|
||||
self.trackable = utils.config_value(app, 'TRACKABLE')
|
||||
self.email_sender = utils.config_value(app, 'EMAIL_SENDER')
|
||||
self.token_authentication_key = utils.config_value(app, 'TOKEN_AUTHENTICATION_KEY')
|
||||
self.token_authentication_header = utils.config_value(app, 'TOKEN_AUTHENTICATION_HEADER')
|
||||
|
||||
+18
-1
@@ -9,9 +9,11 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import current_app as app, redirect, request, session, \
|
||||
render_template
|
||||
from flask.ext.login import login_user, logout_user
|
||||
from flask.ext.login import login_user, logout_user, make_secure_token
|
||||
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
@@ -37,6 +39,21 @@ def _do_login(user, remember=True):
|
||||
"""Performs the login and sends the appropriate signal."""
|
||||
|
||||
if login_user(user, remember):
|
||||
user.remember_token = make_secure_token(user.email, user.password)
|
||||
|
||||
if _security.trackable:
|
||||
old_current, new_current = user.current_login_at, datetime.utcnow()
|
||||
user.last_login_at = old_current or new_current
|
||||
user.current_login_at = new_current
|
||||
|
||||
old_current, new_current = user.current_login_ip, request.remote_addr
|
||||
user.last_login_ip = old_current or new_current
|
||||
user.current_login_ip = new_current
|
||||
|
||||
user.login_count = user.login_count + 1 if user.login_count else 0
|
||||
|
||||
_datastore._save_model(user)
|
||||
|
||||
identity_changed.send(app._get_current_object(),
|
||||
identity=Identity(user.id))
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ class DefaultSecurityTests(SecurityTest):
|
||||
class ConfiguredURLTests(SecurityTest):
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_REGISTERABLE': True,
|
||||
'SECURITY_AUTH_URL': '/custom_auth',
|
||||
'SECURITY_LOGOUT_URL': '/custom_logout',
|
||||
'SECURITY_LOGIN_VIEW': '/custom_login',
|
||||
@@ -142,6 +143,9 @@ class ConfiguredURLTests(SecurityTest):
|
||||
|
||||
|
||||
class RegisterableTests(SecurityTest):
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_REGISTERABLE': True
|
||||
}
|
||||
|
||||
def test_register_valid_user(self):
|
||||
data = dict(email='dude@lp.com', password='password', password_confirm='password')
|
||||
@@ -152,7 +156,8 @@ class RegisterableTests(SecurityTest):
|
||||
|
||||
class ConfirmableTests(SecurityTest):
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_CONFIRMABLE': True
|
||||
'SECURITY_CONFIRMABLE': True,
|
||||
'SECURITY_REGISTERABLE': True
|
||||
}
|
||||
|
||||
def test_register_sends_confirmation_email(self):
|
||||
@@ -216,7 +221,8 @@ class ConfirmableTests(SecurityTest):
|
||||
|
||||
class LoginWithoutImmediateConfirmTests(SecurityTest):
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_CONFIRM_EMAIL': True,
|
||||
'SECURITY_CONFIRMABLE': True,
|
||||
'SECURITY_REGISTERABLE': True,
|
||||
'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True
|
||||
}
|
||||
|
||||
@@ -230,6 +236,10 @@ class LoginWithoutImmediateConfirmTests(SecurityTest):
|
||||
|
||||
class RecoverableTests(SecurityTest):
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_RECOVERABLE': True
|
||||
}
|
||||
|
||||
def test_forgot_post_sends_email_and_sets_required_fields(self):
|
||||
with capture_reset_password_requests() as users:
|
||||
with self.app.mail.record_messages() as outbox:
|
||||
|
||||
Reference in New Issue
Block a user