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"))
     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):
 class ReauthForm(FlaskBBForm):
     password = PasswordField(_('Password'), validators=[
     password = PasswordField(_('Password'), validators=[
@@ -114,11 +105,6 @@ class ResetPasswordForm(FlaskBBForm):
 
 
     submit = SubmitField(_("Reset password"))
     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):
 class RequestActivationForm(FlaskBBForm):
     username = StringField(_("Username"), validators=[
     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,
                                 LoginForm, LoginRecaptchaForm, ReauthForm,
                                 RegisterForm, RequestActivationForm,
                                 RegisterForm, RequestActivationForm,
                                 ResetPasswordForm)
                                 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.exceptions import AuthenticationError
 from flaskbb.extensions import db, limiter
 from flaskbb.extensions import db, limiter
 from flaskbb.user.models import User
 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 flaskbb.utils.tokens import get_token_status
 
 
 from .services import registration
 from .services import registration
-from ..core.auth.password import ResetPasswordService
+from .services.password import ResetPasswordService
 from ..core.auth.registration import (RegistrationService, UserRegistrationInfo)
 from ..core.auth.registration import (RegistrationService, UserRegistrationInfo)
 from ..core.exceptions import ValidationError, StopValidation
 from ..core.exceptions import ValidationError, StopValidation
 from ..core.tokens import TokenError
 from ..core.tokens import TokenError
@@ -168,27 +168,29 @@ class ForgotPassword(MethodView):
     decorators = [anonymous_required]
     decorators = [anonymous_required]
     form = ForgotPasswordForm
     form = ForgotPasswordForm
 
 
+    def __init__(self, password_reset_service_factory):
+        self.password_reset_service_factory = password_reset_service_factory
+
     def get(self):
     def get(self):
         return render_template("auth/forgot_password.html", form=self.form())
         return render_template("auth/forgot_password.html", form=self.form())
 
 
     def post(self):
     def post(self):
         form = self.form()
         form = self.form()
         if form.validate_on_submit():
         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(
                 flash(
                     _(
                     _(
                         "You have entered an username or email address that "
                         "You have entered an username or email address that "
                         "is not linked with your account."
                         "is not linked with your account."
                     ), "danger"
                     ), "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)
         return render_template("auth/forgot_password.html", form=form)
 
 
 
 
@@ -401,11 +403,6 @@ def flaskbb_load_blueprints(app):
             registration_service_factory=registration_service_factory
             registration_service_factory=registration_service_factory
         )
         )
     )
     )
-    register_view(
-        auth,
-        routes=['/reset-password'],
-        view_func=ForgotPassword.as_view('forgot_password')
-    )
 
 
     def reset_service_factory():
     def reset_service_factory():
         token_serializer = FlaskBBTokenSerializer(
         token_serializer = FlaskBBTokenSerializer(
@@ -423,6 +420,15 @@ def flaskbb_load_blueprints(app):
 
 
     register_view(
     register_view(
         auth,
         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>'],
         routes=['/reset-password/<token>'],
         view_func=ResetPassword.as_view(
         view_func=ResetPassword.as_view(
             'reset_password',
             'reset_password',

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

@@ -9,33 +9,17 @@
     :license: BSD, see LICENSE for more details
     :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):
     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
 @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.
     """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 username: The username to whom the email should be sent.
     :param email:  The email address of the user
     :param email:  The email address of the user
     """
     """
-    token = make_token(user_id=user_id, operation="reset_password")
     send_email(
     send_email(
         subject=_("Password Recovery Confirmation"),
         subject=_("Password Recovery Confirmation"),
         recipients=[email],
         recipients=[email],

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

@@ -1,11 +1,12 @@
 import json
 import json
 
 
 import pytest
 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.exceptions import StopValidation, ValidationError
 from flaskbb.core.tokens import Token, TokenActions, TokenError
 from flaskbb.core.tokens import Token, TokenActions, TokenError
 from flaskbb.user.models import User
 from flaskbb.user.models import User
-from werkzeug.security import check_password_hash
 
 
 
 
 class SimpleTokenSerializer:
 class SimpleTokenSerializer:
@@ -64,3 +65,34 @@ class TestPasswordReset(object):
 
 
         service.reset_password(token, Fred.email, "newpasswordwhodis")
         service.reset_password(token, Fred.email, "newpasswordwhodis")
         assert check_password_hash(Fred.password, "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
+        )