From da031b8d15c92c3464aa94880e274468464da209 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Thu, 12 Jul 2012 11:29:42 -0400 Subject: [PATCH] Simplify routing a tiny bit --- flask_security/confirmable.py | 8 +- flask_security/core.py | 26 +++--- flask_security/forms.py | 36 +++++++- flask_security/recoverable.py | 2 +- .../templates/security/confirmations/new.html | 4 +- .../templates/security/passwords/edit.html | 2 +- .../templates/security/passwords/new.html | 4 +- flask_security/views.py | 91 ++++++++++++------- tests/functional_tests.py | 12 +-- 9 files changed, 121 insertions(+), 64 deletions(-) diff --git a/flask_security/confirmable.py b/flask_security/confirmable.py index c1f981b..05d0d3a 100644 --- a/flask_security/confirmable.py +++ b/flask_security/confirmable.py @@ -31,14 +31,14 @@ def send_confirmation_instructions(user, token): :param user: The user to send the instructions to """ - url = url_for('flask_security.confirm', - token=token) + url = url_for('flask_security.confirm_account', token=token) confirmation_link = request.url_root[:-1] + url + ctx = dict(user=user, confirmation_link=confirmation_link) + send_mail('Please confirm your email', user.email, - 'confirmation_instructions', - dict(user=user, confirmation_link=confirmation_link)) + 'confirmation_instructions', ctx) confirm_instructions_sent.send(user, app=app._get_current_object()) diff --git a/flask_security/core.py b/flask_security/core.py index f5148d2..228aece 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -32,10 +32,10 @@ _default_config = { 'AUTH_URL': '/auth', 'LOGOUT_URL': '/logout', 'REGISTER_URL': '/register', - 'FORGOT_URL': '/forgot', - 'RESET_URL': '/reset/', - 'CONFIRM_URL': '/confirm/', + 'RESET_URL': '/reset', + 'CONFIRM_URL': '/confirm', 'LOGIN_VIEW': '/login', + 'CONFIRM_ERROR_VIEW': '/confirm', 'POST_LOGIN_VIEW': '/', 'POST_LOGOUT_VIEW': '/', 'POST_FORGOT_VIEW': '/', @@ -141,20 +141,24 @@ def _create_blueprint(app): if cv('REGISTERABLE', app=app): bp.route(cv('REGISTER_URL', app=app), - methods=['POST'], - endpoint='register')(views.register) + methods=['GET', 'POST'], + endpoint='register')(views.register_user) if cv('RECOVERABLE', app=app): - bp.route(cv('FORGOT_URL', app=app), - methods=['POST'], - endpoint='forgot')(views.forgot) bp.route(cv('RESET_URL', app=app), - methods=['POST'], - endpoint='reset')(views.reset) + methods=['GET', 'POST'], + endpoint='forgot_password')(views.forgot_password) + bp.route(cv('RESET_URL', app=app) + '/', + methods=['GET', 'POST'], + endpoint='reset_password')(views.reset_password) if cv('CONFIRMABLE', app=app): bp.route(cv('CONFIRM_URL', app=app), - endpoint='confirm')(views.confirm) + methods=['GET', 'POST'], + endpoint='send_confirmation')(views.send_confirmation) + bp.route(cv('CONFIRM_URL', app=app) + '/', + methods=['GET', 'POST'], + endpoint='confirm_account')(views.confirm_account) return bp diff --git a/flask_security/forms.py b/flask_security/forms.py index b8cd094..4be9bbe 100644 --- a/flask_security/forms.py +++ b/flask_security/forms.py @@ -9,9 +9,23 @@ :license: MIT, see LICENSE for more details. """ -from flask import request +from flask import request, current_app as app from flask.ext.wtf import Form, TextField, PasswordField, SubmitField, \ - HiddenField, Required, BooleanField, EqualTo, Email + HiddenField, Required, BooleanField, EqualTo, Email, ValidationError +from werkzeug.local import LocalProxy + +from .exceptions import UserNotFoundError + + +# Convenient reference +_datastore = LocalProxy(lambda: app.security.datastore) + + +def valid_user_email(form, field): + try: + _datastore.find_user(email=field.data) + except UserNotFoundError: + raise ValidationError('Invalid email address') class EmailFormMixin(): @@ -20,6 +34,13 @@ class EmailFormMixin(): Email(message="Invalid email address")]) +class UserEmailFormMixin(): + email = TextField("Email Address", + validators=[Required(message="Email not provided"), + Email(message="Invalid email address"), + valid_user_email]) + + class PasswordFormMixin(): password = PasswordField("Password", validators=[Required(message="Password not provided")]) @@ -30,7 +51,16 @@ class PasswordConfirmFormMixin(): validators=[EqualTo('password', message="Passwords do not match")]) -class ForgotPasswordForm(Form, EmailFormMixin): +class ResendConfirmationForm(Form, UserEmailFormMixin): + """The default forgot password form""" + + submit = SubmitField("Resend Confirmation Instructions") + + def to_dict(self): + return dict(email=self.email.data) + + +class ForgotPasswordForm(Form, UserEmailFormMixin): """The default forgot password form""" submit = SubmitField("Recover Password") diff --git a/flask_security/recoverable.py b/flask_security/recoverable.py index 00911fe..ee2144d 100644 --- a/flask_security/recoverable.py +++ b/flask_security/recoverable.py @@ -31,7 +31,7 @@ def send_reset_password_instructions(user, reset_token): :param user: The user to send the instructions to """ - url = url_for('flask_security.reset', + url = url_for('flask_security.reset_password', token=reset_token) reset_link = request.url_root[:-1] + url diff --git a/flask_security/templates/security/confirmations/new.html b/flask_security/templates/security/confirmations/new.html index 03e1e12..7bece48 100644 --- a/flask_security/templates/security/confirmations/new.html +++ b/flask_security/templates/security/confirmations/new.html @@ -1,6 +1,6 @@ -{% include "../messages.html" %} +{% include "security/messages.html" %}

Resend confirmation instructions

-
+ {{ reset_confirmation_form.hidden_tag() }} {{ reset_confirmation_form.email.label }} {{ reset_confirmation_form.email }} {{ reset_confirmation_form.submit }} diff --git a/flask_security/templates/security/passwords/edit.html b/flask_security/templates/security/passwords/edit.html index a0a3ec9..b17de1a 100644 --- a/flask_security/templates/security/passwords/edit.html +++ b/flask_security/templates/security/passwords/edit.html @@ -1,6 +1,6 @@ {% include "../messages.html" %}

Change password

- + {{ 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 }}
diff --git a/flask_security/templates/security/passwords/new.html b/flask_security/templates/security/passwords/new.html index af174f3..c5b2a93 100644 --- a/flask_security/templates/security/passwords/new.html +++ b/flask_security/templates/security/passwords/new.html @@ -1,6 +1,6 @@ -{% include "../messages.html" %} +{% include "security/messages.html" %}

Send reset password instructions

- + {{ forgot_password_form.hidden_tag() }} {{ forgot_password_form.email.label }} {{ forgot_password_form.email }} {{ forgot_password_form.submit }} diff --git a/flask_security/views.py b/flask_security/views.py index 2ef37c7..9b1ace8 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -19,15 +19,16 @@ from werkzeug.datastructures import MultiDict from werkzeug.local import LocalProxy from .confirmable import confirm_by_token, reset_confirmation_token -from .exceptions import TokenExpiredError, UserNotFoundError, \ - ConfirmationError, BadCredentialsError, ResetPasswordError +from .exceptions import TokenExpiredError, ConfirmationError, \ + BadCredentialsError, ResetPasswordError from .forms import LoginForm, RegisterForm, ForgotPasswordForm, \ - ResetPasswordForm + ResetPasswordForm, ResendConfirmationForm from .recoverable import reset_by_token, \ reset_password_reset_token from .signals import user_registered from .tokens import generate_authentication_token -from .utils import get_post_login_redirect, do_flash, get_remember_token +from .utils import get_url, get_post_login_redirect, do_flash, \ + get_remember_token # Convenient references @@ -142,50 +143,71 @@ def logout(): _security.post_logout_view) -def register(): +def register_user(): """View function which handles a registration request.""" form = RegisterForm(csrf_enabled=not app.testing) - # Exit early if the form doesn't validate if form.validate_on_submit(): - # Create user and send signal - user = _datastore.create_user(**form.to_dict()) - confirm_token = None + # Create user + u = _datastore.create_user(**form.to_dict()) # Send confirmation instructions if necessary - if _security.confirmable: - confirm_token = reset_confirmation_token(user) + t = reset_confirmation_token(u) if _security.confirmable else None - user_registered.send(dict(user=user, confirm_token=confirm_token), - app=app._get_current_object()) + data = dict(user=u, confirm_token=t) + user_registered.send(data, app=app._get_current_object()) - _logger.debug('User %s registered' % user) + _logger.debug('User %s registered' % u) # Login the user if allowed if not _security.confirmable or _security.login_without_confirmation: - _do_login(user) + _do_login(u) return redirect(_security.post_register_view or _security.post_login_view) - return redirect(request.referrer or _security.register_url) + return render_template('security/registrations/new.html', + register_user_form=form) -def confirm(token): +def send_confirmation(): + form = ResendConfirmationForm() + + if form.validate_on_submit(): + user = _datastore.find_user(email=form.email.data) + + reset_confirmation_token(user) + + _logger.debug('%s request confirmation instructions' % user) + + msg = 'A new confirmation code has been sent to ' + user.email + do_flash(msg, 'info') + + else: + for key, value in form.errors.items(): + do_flash(value[0], 'error') + + return render_template('security/confirmations/new.html', + reset_confirmation_form=form) + + +def confirm_account(token): """View function which handles a account confirmation request.""" + error = False try: user = confirm_by_token(token) except ConfirmationError, e: + error = True + _logger.debug('Confirmation error: ' + str(e)) do_flash(str(e), 'error') - return redirect('/') # TODO: Don't just redirect to root - except TokenExpiredError, e: + error = True reset_confirmation_token(e.user) @@ -197,7 +219,8 @@ def confirm(token): do_flash(msg, 'error') - return redirect('/') # TODO: Don't redirect to root + if error: + return redirect(get_url(_security.confirm_error_view)) _logger.debug('User %s confirmed' % user) @@ -207,35 +230,35 @@ def confirm(token): _security.post_login_view) -def forgot(): +def forgot_password(): """View function that handles a forgotten password request.""" form = ForgotPasswordForm(csrf_enabled=not app.testing) if form.validate_on_submit(): - try: - user = _datastore.find_user(**form.to_dict()) + user = _datastore.find_user(**form.to_dict()) - reset_password_reset_token(user) + reset_password_reset_token(user) - _logger.debug('%s requested to reset their password' % user) + _logger.debug('%s requested to reset their password' % user) - do_flash('Instructions to reset your password have been ' - 'sent to %s' % user.email, 'success') - - except UserNotFoundError: - _logger.debug('A reset password request was made for %s but ' - 'that email does not exist.' % form.email.data) - - do_flash('The email you provided could not be found', 'error') + do_flash('Instructions to reset your password have been ' + 'sent to %s' % user.email, 'success') return redirect(_security.post_forgot_view) + else: + _logger.debug('A reset password request was made for %s but ' + 'that email does not exist.' % form.email.data) + + for key, value in form.errors.items(): + do_flash(value[0], 'error') + return render_template('security/passwords/new.html', forgot_password_form=form) -def reset(token): +def reset_password(token): """View function that handles a reset password request.""" form = ResetPasswordForm(csrf_enabled=not app.testing) diff --git a/tests/functional_tests.py b/tests/functional_tests.py index 230e504..f55505f 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -267,18 +267,18 @@ class RecoverableTests(SecurityTest): def test_forgot_post_sends_email(self): with capture_reset_password_requests(): with self.app.mail.record_messages() as outbox: - self.client.post('/forgot', data=dict(email='joe@lp.com')) + self.client.post('/reset', data=dict(email='joe@lp.com')) self.assertEqual(len(outbox), 1) def test_forgot_password_invalid_email(self): - r = self.client.post('/forgot', + r = self.client.post('/reset', data=dict(email='larry@lp.com'), follow_redirects=True) - self.assertIn('The email you provided could not be found', r.data) + self.assertIn('Invalid email address', r.data) def test_reset_password_with_valid_token(self): with capture_reset_password_requests() as requests: - r = self.client.post('/forgot', + r = self.client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True) t = requests[0]['token'] @@ -293,7 +293,7 @@ class RecoverableTests(SecurityTest): def test_reset_password_twice_flashes_invalid_token_msg(self): with capture_reset_password_requests() as requests: - self.client.post('/forgot', data=dict(email='joe@lp.com')) + self.client.post('/reset', data=dict(email='joe@lp.com')) t = requests[0]['token'] data = { @@ -316,7 +316,7 @@ class ExpiredResetPasswordTest(SecurityTest): def test_reset_password_with_expired_token(self): with capture_reset_password_requests() as requests: - r = self.client.post('/forgot', + r = self.client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True) t = requests[0]['token']