diff --git a/example/app.py b/example/app.py index 728dff3..16cbbf7 100644 --- a/example/app.py +++ b/example/app.py @@ -114,6 +114,8 @@ def create_sqlalchemy_app(auth_config=None): confirmation_token = db.Column(db.String(255)) confirmation_sent_at = db.Column(db.DateTime()) confirmed_at = db.Column(db.DateTime()) + reset_password_token = db.Column(db.String(255)) + reset_password_sent_at = db.Column(db.DateTime()) roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic')) @@ -147,6 +149,8 @@ def create_mongoengine_app(auth_config=None): confirmation_token = db.StringField(max_length=255) confirmation_sent_at = db.DateTimeField() confirmed_at = db.DateTimeField() + reset_password_token = db.StringField(max_length=255) + reset_password_sent_at = db.DateTimeField() roles = db.ListField(db.ReferenceField(Role), default=[]) Security(app, MongoEngineUserDatastore(db, User, Role)) diff --git a/example/templates/register.html b/example/templates/register.html index 3bcde60..debcaff 100644 --- a/example/templates/register.html +++ b/example/templates/register.html @@ -1,10 +1,10 @@ {% include "_messages.html" %} {% include "_nav.html" %}
- {{ form.hidden_tag() }} - {{ form.email.label }} {{ form.email }}
- {{ form.password.label }} {{ form.password }}
- {{ form.password_confirm.label }} {{ form.password_confirm }}
- {{ form.submit }} + {{ register_user_form.hidden_tag() }} + {{ register_user_form.email.label }} {{ register_user_form.email }}
+ {{ register_user_form.password.label }} {{ register_user_form.password }}
+ {{ register_user_form.password_confirm.label }} {{ register_user_form.password_confirm }}
+ {{ register_user_form.submit }}

{{ content }}

diff --git a/flask_security/confirmable.py b/flask_security/confirmable.py index bf40ecf..9c1a849 100644 --- a/flask_security/confirmable.py +++ b/flask_security/confirmable.py @@ -3,17 +3,18 @@ from datetime import datetime from flask import current_app, request, url_for from flask.ext.security.exceptions import UserNotFoundError, \ - ConfirmationError, ConfirmationExpiredError + ConfirmationError, TokenExpiredError from flask.ext.security.utils import generate_token, send_mail from werkzeug.local import LocalProxy 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') + raise ConfirmationError('Confirmation token required') return security.datastore.find_user(confirmation_token=token) @@ -70,14 +71,18 @@ def confirmation_token_is_expired(user): def confirm_by_token(token): - user = find_user_by_confirmation_token(token) + try: + user = find_user_by_confirmation_token(token) + except UserNotFoundError: + raise ConfirmationError('Invalid confirmation token') if confirmation_token_is_expired(user): - raise ConfirmationExpiredError(user=user) + raise TokenExpiredError(message='Confirmation token is expired', + user=user) + user.confirmation_token = None + user.confirmation_sent_at = None user.confirmed_at = datetime.utcnow() - #user.confirmation_token = None - #user.confirmation_sent_at = None security.datastore._save_model(user) diff --git a/flask_security/core.py b/flask_security/core.py index 79eec4e..73a20e8 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -33,21 +33,25 @@ _default_config = { 'AUTH_PROVIDER': 'flask.ext.security::AuthenticationProvider', 'LOGIN_FORM': 'flask.ext.security::LoginForm', 'REGISTER_FORM': 'flask.ext.security::RegisterForm', + 'RESET_PASSWORD_FORM': 'flask.ext.security::ResetPasswordForm', + 'FORGOT_PASSWORD_FORM': 'flask.ext.security::ForgotPasswordForm', 'AUTH_URL': '/auth', 'LOGOUT_URL': '/logout', 'REGISTER_URL': '/register', + 'FORGOT_URL': '/forgot', 'RESET_URL': '/reset', 'CONFIRM_URL': '/confirm', 'LOGIN_VIEW': '/login', 'POST_LOGIN_VIEW': '/', 'POST_LOGOUT_VIEW': '/', + 'POST_FORGOT_VIEW': '/', 'POST_REGISTER_VIEW': None, 'POST_CONFIRM_VIEW': None, - 'RESET_PASSWORD_WITHIN': 10, 'DEFAULT_ROLES': [], - 'LOGIN_WITHOUT_CONFIRMATION': False, 'CONFIRM_EMAIL': False, 'CONFIRM_EMAIL_WITHIN': '5 days', + 'RESET_PASSWORD_WITHIN': '2 days', + 'LOGIN_WITHOUT_CONFIRMATION': False, 'EMAIL_SENDER': 'no-reply@localhost' } @@ -198,7 +202,7 @@ class Security(object): self.init_app(app, datastore, **kwargs) def init_app(self, app, datastore, - registerable=True, recoverable=False, template_folder=None): + registerable=True, recoverable=True, template_folder=None): """Initializes the Flask-Security extension for the specified application and datastore implentation. @@ -227,25 +231,32 @@ class Security(object): self.datastore = datastore self.LoginForm = utils.get_class_from_string(app, 'LOGIN_FORM') self.RegisterForm = utils.get_class_from_string(app, 'REGISTER_FORM') + self.ResetPasswordForm = utils.get_class_from_string(app, 'RESET_PASSWORD_FORM') + self.ForgotPasswordForm = utils.get_class_from_string(app, 'FORGOT_PASSWORD_FORM') self.auth_url = utils.config_value(app, 'AUTH_URL') self.logout_url = utils.config_value(app, 'LOGOUT_URL') self.reset_url = utils.config_value(app, 'RESET_URL') self.register_url = utils.config_value(app, 'REGISTER_URL') self.confirm_url = utils.config_value(app, 'CONFIRM_URL') + self.forgot_url = utils.config_value(app, 'FORGOT_URL') 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.post_forgot_view = utils.config_value(app, 'POST_FORGOT_VIEW') 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') + self.confirm_email_within_text = utils.config_value(app, 'CONFIRM_EMAIL_WITHIN') values = self.confirm_email_within_text.split() self.confirm_email_within = timedelta(**{values[1]: int(values[0])}) + self.reset_password_within_text = utils.config_value(app, 'RESET_PASSWORD_WITHIN') + values = self.reset_password_within_text.split() + self.reset_password_within = timedelta(**{values[1]: int(values[0])}) + identity_loaded.connect_via(app)(on_identity_loaded) bp = Blueprint('flask_security', __name__, template_folder='templates') @@ -257,29 +268,37 @@ class Security(object): bp.route(self.logout_url, endpoint='logout')(login_required(views.logout)) - self.setup_register(bp) if registerable else None - self.setup_reset(bp) if recoverable else None - self.setup_confirm(bp) if self.confirm_email else None + self.setup_registerable(bp) if registerable else None + self.setup_recoverable(bp) if recoverable else None + self.setup_confirmable(bp) if self.confirm_email else None app.register_blueprint(bp, url_prefix=utils.config_value(app, 'URL_PREFIX')) app.security = self - def setup_register(self, bp): + def setup_registerable(self, bp): bp.route(self.register_url, methods=['POST'], endpoint='register')(views.register) - def setup_reset(self, bp): + def setup_recoverable(self, bp): + bp.route(self.forgot_url, + methods=['POST'], + endpoint='forgot')(views.forgot) bp.route(self.reset_url, methods=['POST'], endpoint='reset')(views.reset) - def setup_confirm(self, bp): + def setup_confirmable(self, bp): bp.route(self.confirm_url, endpoint='confirm')(views.confirm) +class ForgotPasswordForm(Form): + email = TextField("Email Address", + validators=[Required(message="Email not provided")]) + + class LoginForm(Form): """The default login form""" @@ -311,7 +330,8 @@ class RegisterForm(Form): class ResetPasswordForm(Form): - token = HiddenField() + reset_password_token = HiddenField(validators=[Required()]) + email = HiddenField(validators=[Required()]) password = PasswordField("Password", validators=[Required(message="Password not provided")]) password_confirm = PasswordField("Retype Password", diff --git a/flask_security/datastore.py b/flask_security/datastore.py index 4e5ef96..c47a59d 100644 --- a/flask_security/datastore.py +++ b/flask_security/datastore.py @@ -99,7 +99,7 @@ class UserDatastore(object): user = self._do_find_user(**kwargs) if user: return user - raise exceptions.UserNotFoundError() + raise exceptions.UserNotFoundError('Parameters=%s' % kwargs) def find_role(self, role): """Returns a role based on its name. diff --git a/flask_security/exceptions.py b/flask_security/exceptions.py index 3b00fac..a060d8e 100644 --- a/flask_security/exceptions.py +++ b/flask_security/exceptions.py @@ -10,68 +10,73 @@ """ -class BadCredentialsError(Exception): +class SecurityError(Exception): + def __init__(self, message=None, user=None): + super(SecurityError, self).__init__(message) + self.user = user + + +class BadCredentialsError(SecurityError): """Raised when an authentication attempt fails due to an error with the provided credentials. """ -class AuthenticationError(Exception): +class AuthenticationError(SecurityError): """Raised when an authentication attempt fails due to invalid configuration or an unknown reason. """ -class UserNotFoundError(Exception): +class UserNotFoundError(SecurityError): """Raised by a user datastore when there is an attempt to find a user by their identifier, often username or email, and the user is not found. """ -class RoleNotFoundError(Exception): +class RoleNotFoundError(SecurityError): """Raised by a user datastore when there is an attempt to find a role and the role cannot be found. """ -class UserIdNotFoundError(Exception): +class UserIdNotFoundError(SecurityError): """Raised by a user datastore when there is an attempt to find a user by ID and the user is not found. """ -class UserDatastoreError(Exception): +class UserDatastoreError(SecurityError): """Raised when a user datastore experiences an unexpected error """ -class UserCreationError(Exception): +class UserCreationError(SecurityError): """Raised when an error occurs when creating a user """ -class RoleCreationError(Exception): +class RoleCreationError(SecurityError): """Raised when an error occurs when creating a role """ -class ConfirmationError(Exception): - """Raised when an unknown confirmation error occurs +class ConfirmationError(SecurityError): + """Raised when an confirmation error occurs """ -class ConfirmationExpiredError(Exception): +class TokenExpiredError(SecurityError): """Raised when a user attempts to confirm their email but their token has expired """ - def __init__(self, user=None): - super(ConfirmationExpiredError, self).__init__() - self.user = user -class ConfirmationRequiredError(Exception): +class ConfirmationRequiredError(SecurityError): """Raised when a user attempts to login but requires confirmation """ - def __init__(self, user=None): - super(ConfirmationRequiredError, self).__init__() - self.user = user + + +class ResetPasswordError(SecurityError): + """Raised when a password reset error occurs + """ diff --git a/flask_security/recoverable.py b/flask_security/recoverable.py index cee2a0f..addb248 100644 --- a/flask_security/recoverable.py +++ b/flask_security/recoverable.py @@ -1,34 +1,82 @@ -from datetime import datetime, timedelta +from datetime import datetime -from flask import current_app -from flask.ext.security.utils import generate_token +from flask import current_app, request, url_for +from flask.ext.security.exceptions import ResetPasswordError, \ + UserNotFoundError, TokenExpiredError +from flask.ext.security.signals import password_reset_requested +from flask.ext.security.utils import generate_token, send_mail +from werkzeug.local import LocalProxy + +security = LocalProxy(lambda: current_app.security) + +logger = LocalProxy(lambda: current_app.logger) -def reset_password_period_valid(user): - sent_at = user.reset_password_sent_at - reset_within = int(current_app.security.reset_password_within) - days_ago = datetime.utcnow() - timedelta(days=reset_within) +def find_user_by_reset_token(token): + if not token: + raise ResetPasswordError('Reset password token required') + return security.datastore.find_user(reset_password_token=token) - return (sent_at is not None) and \ - (sent_at >= days_ago) + +def send_reset_password_instructions(user): + url = url_for('flask_security.reset', + reset_token=user.reset_password_token) + + reset_link = request.url_root[:-1] + url + + send_mail('Password reset instructions', + user.email, + 'reset_instructions', + dict(user=user, reset_link=reset_link)) + + return True def generate_reset_password_token(user): - user.reset_password_token = generate_token() - user.reset_password_sent_at = datetime.utcnow() - current_app.security.datastore._save_model(user) + while True: + token = generate_token() + try: + find_user_by_reset_token(token) + except UserNotFoundError: + break + + now = datetime.utcnow() + + try: + user['reset_password_token'] = token + user['reset_password_token'] = now + except TypeError: + user.reset_password_token = token + user.reset_password_sent_at = now + + return user -def clear_reset_password_token(user): +def password_reset_token_is_expired(user): + token_expires = datetime.utcnow() - security.reset_password_within + return user.reset_password_sent_at < token_expires + + +def reset_by_token(token, email, password): + try: + user = find_user_by_reset_token(token) + except UserNotFoundError: + raise ResetPasswordError('Invalid reset password token') + + if password_reset_token_is_expired(user): + raise TokenExpiredError('Reset password token is expired', user) + user.reset_password_token = None user.reset_password_sent_at = None + user.password = security.pwd_context.encrypt(password) + + security.datastore._save_model(user) + + return user -def send_reset_password_instructions(): - pass - - -def should_generate_reset_token(user): - return (user.reset_password_token is None) or \ - (not reset_password_period_valid(user)) +def reset_password_reset_token(user): + security.datastore._save_model(generate_reset_password_token(user)) + send_reset_password_instructions(user) + password_reset_requested.send(user, app=current_app._get_current_object()) diff --git a/flask_security/signals.py b/flask_security/signals.py index 6c257ad..3990126 100644 --- a/flask_security/signals.py +++ b/flask_security/signals.py @@ -2,4 +2,6 @@ import blinker signals = blinker.Namespace() -user_registered = signals.signal("user-register") +user_registered = signals.signal("user-registered") + +password_reset_requested = signals.signal("password-reset-requested") diff --git a/flask_security/templates/confirmed.html b/flask_security/templates/confirmed.html deleted file mode 100644 index e69de29..0000000 diff --git a/flask_security/templates/register.html b/flask_security/templates/register.html deleted file mode 100644 index c875955..0000000 --- a/flask_security/templates/register.html +++ /dev/null @@ -1,7 +0,0 @@ -
- {{ form.hidden_tag() }} - {{ form.email.label }} {{ form.email }}
- {{ form.password.label }} {{ form.password }}
- {{ form.password_confirm.label }} {{ form.password_confirm }}
- {{ form.submit }} -
\ No newline at end of file diff --git a/flask_security/templates/reset.html b/flask_security/templates/reset.html deleted file mode 100644 index 744448b..0000000 --- a/flask_security/templates/reset.html +++ /dev/null @@ -1,6 +0,0 @@ -
- {{ form.hidden_tag() }} - {{ form.password.label }} {{ form.password }}
- {{ form.password_confirm.label }} {{ form.password_confirm }}
- {{ form.submit }} -
\ No newline at end of file diff --git a/flask_security/templates/security/confirmations/new.html b/flask_security/templates/security/confirmations/new.html new file mode 100644 index 0000000..36a1ca0 --- /dev/null +++ b/flask_security/templates/security/confirmations/new.html @@ -0,0 +1 @@ +Resend confirmation instructions... \ No newline at end of file diff --git a/flask_security/templates/email/confirmation_instructions.html b/flask_security/templates/security/email/confirmation_instructions.html similarity index 100% rename from flask_security/templates/email/confirmation_instructions.html rename to flask_security/templates/security/email/confirmation_instructions.html diff --git a/flask_security/templates/email/confirmation_instructions.txt b/flask_security/templates/security/email/confirmation_instructions.txt similarity index 100% rename from flask_security/templates/email/confirmation_instructions.txt rename to flask_security/templates/security/email/confirmation_instructions.txt diff --git a/flask_security/templates/security/email/reset_instructions.html b/flask_security/templates/security/email/reset_instructions.html new file mode 100644 index 0000000..fd0b48d --- /dev/null +++ b/flask_security/templates/security/email/reset_instructions.html @@ -0,0 +1 @@ +

Click here to reset your password

\ No newline at end of file diff --git a/flask_security/templates/security/email/reset_instructions.txt b/flask_security/templates/security/email/reset_instructions.txt new file mode 100644 index 0000000..91ac288 --- /dev/null +++ b/flask_security/templates/security/email/reset_instructions.txt @@ -0,0 +1,3 @@ +Click the link below to reset your password: + +{{ reset_link }} \ No newline at end of file diff --git a/flask_security/templates/security/email/reset_notice.html b/flask_security/templates/security/email/reset_notice.html new file mode 100644 index 0000000..536e296 --- /dev/null +++ b/flask_security/templates/security/email/reset_notice.html @@ -0,0 +1 @@ +

Your password has been reset

\ No newline at end of file diff --git a/flask_security/templates/security/email/reset_notice.txt b/flask_security/templates/security/email/reset_notice.txt new file mode 100644 index 0000000..a3fa0b4 --- /dev/null +++ b/flask_security/templates/security/email/reset_notice.txt @@ -0,0 +1 @@ +Your password has been reset \ No newline at end of file diff --git a/flask_security/templates/login.html b/flask_security/templates/security/logins/new.html similarity index 100% rename from flask_security/templates/login.html rename to flask_security/templates/security/logins/new.html diff --git a/flask_security/templates/security/passwords/edit.html b/flask_security/templates/security/passwords/edit.html new file mode 100644 index 0000000..0c3ea67 --- /dev/null +++ b/flask_security/templates/security/passwords/edit.html @@ -0,0 +1,6 @@ +
+ {{ reset_password_form.hidden_tag() }} + {{ reset_password_form.password.label }} {{ reset_password_form.password }}
+ {{ reset_password_form.password_confirm.label }} {{ reset_password_form.password_confirm }}
+ {{ reset_password_form.submit }} +
\ No newline at end of file diff --git a/flask_security/templates/security/passwords/new.html b/flask_security/templates/security/passwords/new.html new file mode 100644 index 0000000..c8f6a17 --- /dev/null +++ b/flask_security/templates/security/passwords/new.html @@ -0,0 +1,5 @@ +
+ {{ forgot_password_form.hidden_tag() }} + {{ forgot_password_form.email.label }} {{ forgot_password_form.email }} + {{ forgot_password_form.submit }} +
\ No newline at end of file diff --git a/flask_security/templates/security/registrations/edit.html b/flask_security/templates/security/registrations/edit.html new file mode 100644 index 0000000..a37845e --- /dev/null +++ b/flask_security/templates/security/registrations/edit.html @@ -0,0 +1,8 @@ +
+ {{ edit_user_form.hidden_tag() }} + {{ edit_user_form.email.label }} {{ edit_user_form.email }}
+ {{ edit_user_form.password.label }} {{ edit_user_form.password }}
+ {{ edit_user_form.password_confirm.label }} {{ edit_user_form.password_confirm }}
+ {{ edit_user_form.current_password.label }} {{ edit_user_form.current_password }}
+ {{ edit_user_form.submit }} +
\ No newline at end of file diff --git a/flask_security/templates/security/registrations/new.html b/flask_security/templates/security/registrations/new.html new file mode 100644 index 0000000..c9d7bf7 --- /dev/null +++ b/flask_security/templates/security/registrations/new.html @@ -0,0 +1,7 @@ +
+ {{ register_user_form.hidden_tag() }} + {{ register_user_form.email.label }} {{ register_user_form.email }}
+ {{ register_user_form.password.label }} {{ register_user_form.password }}
+ {{ register_user_form.password_confirm.label }} {{ register_user_form.password_confirm }}
+ {{ register_user_form.submit }} +
\ No newline at end of file diff --git a/flask_security/utils.py b/flask_security/utils.py index 071f5ab..e467c5d 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -16,7 +16,8 @@ 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 +from flask.ext.security.signals import user_registered, password_reset_requested +from werkzeug.exceptions import BadRequest def generate_token(): @@ -74,8 +75,9 @@ def send_mail(subject, recipient, template, context): sender=current_app.security.email_sender, recipients=[recipient]) - msg.body = render_template('email/%s.txt' % template, **context) - msg.html = render_template('email/%s.html' % template, **context) + base = 'security/email' + msg.body = render_template('%s/%s.txt' % (base, template), **context) + msg.html = render_template('%s/%s.html' % (base, template), **context) current_app.mail.send(msg) @@ -97,3 +99,29 @@ def capture_registrations(confirmation_sent_at=None): yield users finally: user_registered.disconnect(_on) + + +@contextmanager +def capture_reset_password_requests(reset_password_sent_at=None): + users = [] + + def _on(user, app): + if reset_password_sent_at: + user.reset_password_sent_at = reset_password_sent_at + current_app.security.datastore._save_model(user) + + users.append(user) + + password_reset_requested.connect(_on) + + try: + yield users + finally: + password_reset_requested.disconnect(_on) + + +def get_arg_or_bad_request(context, name): + rv = context.get(name, None) + if not rv: + raise BadRequest() + return rv diff --git a/flask_security/views.py b/flask_security/views.py index 8c9440c..d3fed60 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -9,14 +9,16 @@ :license: MIT, see LICENSE for more details. """ -from flask import current_app, redirect, request, session +from flask import current_app, redirect, request, session, render_template from flask.ext.login import login_user, logout_user from flask.ext.principal import Identity, AnonymousIdentity, identity_changed from flask.ext.security.confirmable import confirm_by_token, \ confirmation_token_is_expired, requires_confirmation, \ reset_confirmation_token, send_confirmation_instructions -from flask.ext.security.exceptions import ConfirmationExpiredError, \ - ConfirmationError, BadCredentialsError +from flask.ext.security.recoverable import reset_by_token, \ + reset_password_reset_token +from flask.ext.security.exceptions import TokenExpiredError, UserNotFoundError, \ + ConfirmationError, BadCredentialsError, ResetPasswordError from flask.ext.security.utils import get_post_login_redirect, do_flash from flask.ext.security.signals import user_registered from werkzeug.local import LocalProxy @@ -70,7 +72,7 @@ def authenticate(): do_flash(msg, 'error') - logger.debug('Unsuccessful authentication attempt: %s. ' % msg) + logger.debug('Unsuccessful authentication attempt: %s' % msg) return redirect(request.referrer or security.login_manager.login_view) @@ -129,15 +131,16 @@ def confirm(): """View function which confirms a user's email address using a token taken from the value of the `confirmation_token` query string argument. """ + try: token = request.args.get('confirmation_token', None) - confirm_by_token(token) + user = confirm_by_token(token) except ConfirmationError, e: do_flash(str(e), 'error') return redirect('/') # TODO: Don't just redirect to root - except ConfirmationExpiredError, e: + except TokenExpiredError, e: reset_confirmation_token(e.user) msg = 'You did not confirm your email within %s. ' \ @@ -146,17 +149,44 @@ def confirm(): do_flash(msg, 'error') - return redirect('/') + return redirect('/') # TODO: Don't redirect to root + logger.debug('User %s confirmed' % user) do_flash('Your email has been confirmed. You may now log in.', 'success') return redirect(security.post_confirm_view or security.post_login_view) +def forgot(): + form = security.ForgotPasswordForm(csrf_enabled=not current_app.testing) + + if form.validate_on_submit(): + try: + user = security.datastore.find_user(email=form.email.data) + reset_password_reset_token(user) + do_flash('Instructions to reset your password have been sent to %s' % user.email, 'success') + + except UserNotFoundError: + do_flash('The email you provided could not be found', 'error') + + return redirect(security.post_forgot_view) + + return render_template('security/passwords/new.html', forgot_password_form=form) + + def reset(): - # user = something - # if reset_password_period_valid_for_user(user): - # user.reset_password_sent_at = datetime.utcnow() - # user.reset_password_token = token - # current_app.security.datastore._save_model(user) - pass + form = security.ResetPasswordForm(csrf_enabled=not current_app.testing) + + if form.validate_on_submit(): + try: + reset_by_token(token=form.reset_password_token.data, + email=form.email.data, + password=form.password.data) + + except ResetPasswordError, e: + do_flash(str(e)) + + except TokenExpiredError, e: + do_flash('You did not reset your password within %s.' % security.reset_password_within_text) + + return redirect(request.referrer) diff --git a/tests/functional_tests.py b/tests/functional_tests.py index 7575468..3dca3d9 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -3,7 +3,8 @@ import unittest from datetime import datetime, timedelta -from flask.ext.security.utils import capture_registrations +from flask.ext.security.utils import capture_registrations, \ + capture_reset_password_requests from example import app @@ -33,8 +34,9 @@ class SecurityTest(unittest.TestCase): follow_redirects=follow_redirects, content_type=content_type or 'application/x-www-form-urlencoded') - def register(self, email, password, endpoint=None): - return self._post(endpoint or '/register') + 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 authenticate(self, email, password, endpoint=None): data = dict(email=email, password=password) @@ -113,12 +115,6 @@ class DefaultSecurityTests(SecurityTest): r = self._get('/admin', follow_redirects=True) self.assertIn('