Browse Source

Use reset password service to send token as well

Alec Nikolas Reiter 7 years ago
parent
commit
23f2c3d19d

+ 0 - 14
flaskbb/auth/forms.py

@@ -72,15 +72,6 @@ class RegisterForm(FlaskBBForm):
 
     submit = SubmitField(_("Register"))
 
-    def save(self):
-        user = User(username=self.username.data,
-                    email=self.email.data,
-                    password=self.password.data,
-                    date_joined=time_utcnow(),
-                    primary_group_id=4,
-                    language=self.language.data)
-        return user.save()
-
 
 class ReauthForm(FlaskBBForm):
     password = PasswordField(_('Password'), validators=[
@@ -114,11 +105,6 @@ class ResetPasswordForm(FlaskBBForm):
 
     submit = SubmitField(_("Reset password"))
 
-    def validate_email(self, field):
-        email = User.query.filter_by(email=field.data).first()
-        if not email:
-            raise ValidationError(_("Wrong email address."))
-
 
 class RequestActivationForm(FlaskBBForm):
     username = StringField(_("Username"), validators=[

+ 56 - 0
flaskbb/auth/services/password.py

@@ -0,0 +1,56 @@
+"""
+    flaskbb.auth.password
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Password reset manager
+
+    :copyright: (c) 2014-2018 the FlaskBB Team.
+    :license: BSD, see LICENSE for more details
+"""
+
+from ...core.auth.password import ResetPasswordService as _ResetPasswordService
+from ...core.exceptions import StopValidation, ValidationError
+from ...core.tokens import Token, TokenActions, TokenError
+from ...email import send_reset_token
+
+
+class ResetPasswordService(_ResetPasswordService):
+
+    def __init__(self, token_serializer, users, token_verifiers):
+        self.token_serializer = token_serializer
+        self.users = users
+        self.token_verifiers = token_verifiers
+
+    def initiate_password_reset(self, email):
+        user = self.users.query.filter_by(email=email).first()
+
+        if user is None:
+            raise ValidationError('email', 'Invalid email')
+
+        token = self.token_serializer.dumps(
+            Token(user_id=user.id, operation=TokenActions.RESET_PASSWORD)
+        )
+
+        send_reset_token.delay(
+            token=token, username=user.username, email=user.email
+        )
+
+    def reset_password(self, token, email, new_password):
+        token = self.token_serializer.loads(token)
+        if token.operation != TokenActions.RESET_PASSWORD:
+            raise TokenError.invalid()
+        self._verify_token(token, email)
+        user = self.users.query.get(token.user_id)
+        user.password = new_password
+
+    def _verify_token(self, token, email):
+        errors = []
+
+        for verifier in self.token_verifiers:
+            try:
+                verifier(token, email=email)
+            except ValidationError as e:
+                errors.append((e.attribute, e.reason))
+
+        if errors:
+            raise StopValidation(errors)

+ 21 - 15
flaskbb/auth/views.py

@@ -22,7 +22,7 @@ from flaskbb.auth.forms import (AccountActivationForm, ForgotPasswordForm,
                                 LoginForm, LoginRecaptchaForm, ReauthForm,
                                 RegisterForm, RequestActivationForm,
                                 ResetPasswordForm)
-from flaskbb.email import send_activation_token, send_reset_token
+from flaskbb.email import send_activation_token
 from flaskbb.exceptions import AuthenticationError
 from flaskbb.extensions import db, limiter
 from flaskbb.user.models import User
@@ -35,7 +35,7 @@ from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.tokens import get_token_status
 
 from .services import registration
-from ..core.auth.password import ResetPasswordService
+from .services.password import ResetPasswordService
 from ..core.auth.registration import (RegistrationService, UserRegistrationInfo)
 from ..core.exceptions import ValidationError, StopValidation
 from ..core.tokens import TokenError
@@ -168,27 +168,29 @@ class ForgotPassword(MethodView):
     decorators = [anonymous_required]
     form = ForgotPasswordForm
 
+    def __init__(self, password_reset_service_factory):
+        self.password_reset_service_factory = password_reset_service_factory
+
     def get(self):
         return render_template("auth/forgot_password.html", form=self.form())
 
     def post(self):
         form = self.form()
         if form.validate_on_submit():
-            user = User.query.filter_by(email=form.email.data).first()
 
-            if user:
-                send_reset_token.delay(
-                    user_id=user.id, username=user.username, email=user.email
-                )
-                flash(_("Email sent! Please check your inbox."), "info")
-                return redirect(url_for("auth.forgot_password"))
-            else:
+            try:
+                self.password_reset_service_factory().initiate_password_reset(form.email.data)
+            except ValidationError:
                 flash(
                     _(
                         "You have entered an username or email address that "
                         "is not linked with your account."
                     ), "danger"
                 )
+            else:
+                flash(_("Email sent! Please check your inbox."), "info")
+                return redirect(url_for("auth.forgot_password"))
+
         return render_template("auth/forgot_password.html", form=form)
 
 
@@ -401,11 +403,6 @@ def flaskbb_load_blueprints(app):
             registration_service_factory=registration_service_factory
         )
     )
-    register_view(
-        auth,
-        routes=['/reset-password'],
-        view_func=ForgotPassword.as_view('forgot_password')
-    )
 
     def reset_service_factory():
         token_serializer = FlaskBBTokenSerializer(
@@ -423,6 +420,15 @@ def flaskbb_load_blueprints(app):
 
     register_view(
         auth,
+        routes=['/reset-password'],
+        view_func=ForgotPassword.as_view(
+            'forgot_password',
+            password_reset_service_factory=reset_service_factory
+        )
+    )
+
+    register_view(
+        auth,
         routes=['/reset-password/<token>'],
         view_func=ResetPassword.as_view(
             'reset_password',

+ 8 - 24
flaskbb/core/auth/password.py

@@ -9,33 +9,17 @@
     :license: BSD, see LICENSE for more details
 """
 
-from ..exceptions import StopValidation, ValidationError
-from ..tokens import TokenActions, TokenError
+from abc import abstractmethod
 
+from ..._compat import ABC
 
-class ResetPasswordService(object):
 
-    def __init__(self, token_serializer, users, token_verifiers):
-        self.token_serializer = token_serializer
-        self.users = users
-        self.token_verifiers = token_verifiers
+class ResetPasswordService(ABC):
 
-    def verify_token(self, token, email):
-        errors = []
-
-        for verifier in self.token_verifiers:
-            try:
-                verifier(token, email=email)
-            except ValidationError as e:
-                errors.append((e.attribute, e.reason))
-
-        if errors:
-            raise StopValidation(errors)
+    @abstractmethod
+    def initiate_password_reset(self, email):
+        pass
 
+    @abstractmethod
     def reset_password(self, token, email, new_password):
-        token = self.token_serializer.loads(token)
-        if token.operation != TokenActions.RESET_PASSWORD:
-            raise TokenError.invalid()
-        self.verify_token(token, email)
-        user = self.users.query.get(token.user_id)
-        user.password = new_password
+        pass

+ 2 - 3
flaskbb/email.py

@@ -21,14 +21,13 @@ logger = logging.getLogger(__name__)
 
 
 @celery.task
-def send_reset_token(user_id, username, email):
+def send_reset_token(token, username, email):
     """Sends the reset token to the user's email address.
 
-    :param user_id: The user id. Used to generate the reset token.
+    :param token: The token to send to the user
     :param username: The username to whom the email should be sent.
     :param email:  The email address of the user
     """
-    token = make_token(user_id=user_id, operation="reset_password")
     send_email(
         subject=_("Password Recovery Confirmation"),
         recipients=[email],

+ 34 - 2
tests/core/auth/test_password.py → tests/unit/auth/test_password.py

@@ -1,11 +1,12 @@
 import json
 
 import pytest
-from flaskbb.core.auth import password
+from werkzeug.security import check_password_hash
+
+from flaskbb.auth.services import password
 from flaskbb.core.exceptions import StopValidation, ValidationError
 from flaskbb.core.tokens import Token, TokenActions, TokenError
 from flaskbb.user.models import User
-from werkzeug.security import check_password_hash
 
 
 class SimpleTokenSerializer:
@@ -64,3 +65,34 @@ class TestPasswordReset(object):
 
         service.reset_password(token, Fred.email, "newpasswordwhodis")
         assert check_password_hash(Fred.password, "newpasswordwhodis")
+
+    # need fred to initiate Users
+    def test_initiate_raises_if_user_doesnt_exist(self, Fred):
+        service = password.ResetPasswordService(
+            SimpleTokenSerializer, User, []
+        )
+        with pytest.raises(ValidationError) as excinfo:
+            service.initiate_password_reset('lol@doesnt.exist')
+
+        assert excinfo.value.attribute == 'email'
+        assert excinfo.value.reason == 'Invalid email'
+
+    def test_calls_send_reset_token_successfully_if_user_exists(
+            self, Fred, mocker
+    ):
+        service = password.ResetPasswordService(
+            SimpleTokenSerializer, User, []
+        )
+        mock = mocker.MagicMock()
+
+        with mocker.patch(
+            'flaskbb.auth.services.password.send_reset_token.delay', mock
+        ):
+            service.initiate_password_reset(Fred.email)
+
+        token = SimpleTokenSerializer.dumps(
+            Token(user_id=Fred.id, operation=TokenActions.RESET_PASSWORD)
+        )
+        mock.assert_called_once_with(
+            token=token, username=Fred.username, email=Fred.email
+        )