From 59c41aec7326ec6e2c2afa56ab3dd849254c5f0a Mon Sep 17 00:00:00 2001 From: bomba1990 Date: Mon, 17 Feb 2020 20:08:43 -0300 Subject: [PATCH 01/13] Moved some validations to the serializer --- django_rest_passwordreset/serializers.py | 33 ++++++++++++++++++++-- django_rest_passwordreset/views.py | 35 +----------------------- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/django_rest_passwordreset/serializers.py b/django_rest_passwordreset/serializers.py index 5d87447..935e36a 100644 --- a/django_rest_passwordreset/serializers.py +++ b/django_rest_passwordreset/serializers.py @@ -1,6 +1,13 @@ +from datetime import timedelta + +from django.http import Http404 from django.utils.translation import ugettext_lazy as _ +from django_rest_passwordreset.models import get_password_reset_token_expiry_time +from django.utils import timezone from rest_framework import serializers +from rest_framework.generics import get_object_or_404 +from . import models __all__ = [ 'EmailSerializer', @@ -13,10 +20,32 @@ class EmailSerializer(serializers.Serializer): email = serializers.EmailField() -class PasswordTokenSerializer(serializers.Serializer): +class PasswordValidateMixin: + def validate(self, data): + token = data.get('token') + + # get token validation time + password_reset_token_validation_time = get_password_reset_token_expiry_time() + + # find token + reset_password_token = get_object_or_404(models.ResetPasswordToken, key=token) + + # check expiry date + expiry_date = reset_password_token.created_at + timedelta( + hours=password_reset_token_validation_time) + + if timezone.now() > expiry_date: + # delete expired token + reset_password_token.delete() + raise Http404(_("The token has expired")) + return data + + +class PasswordTokenSerializer(PasswordValidateMixin, serializers.Serializer): password = serializers.CharField(label=_("Password"), style={'input_type': 'password'}) token = serializers.CharField() -class TokenSerializer(serializers.Serializer): +class TokenSerializer(PasswordValidateMixin, serializers.Serializer): token = serializers.CharField() + diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index 829d94c..e22d50d 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.conf import settings -from rest_framework import status, serializers, exceptions +from rest_framework import status, exceptions from rest_framework.generics import GenericAPIView from rest_framework.response import Response @@ -40,25 +40,6 @@ class ResetPasswordValidateToken(GenericAPIView): def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) - token = serializer.validated_data['token'] - - # get token validation time - password_reset_token_validation_time = get_password_reset_token_expiry_time() - - # find token - reset_password_token = ResetPasswordToken.objects.filter(key=token).first() - - if reset_password_token is None: - return Response({'status': 'notfound'}, status=status.HTTP_404_NOT_FOUND) - - # check expiry date - expiry_date = reset_password_token.created_at + timedelta(hours=password_reset_token_validation_time) - - if timezone.now() > expiry_date: - # delete expired token - reset_password_token.delete() - return Response({'status': 'expired'}, status=status.HTTP_404_NOT_FOUND) - return Response({'status': 'OK'}) @@ -76,23 +57,9 @@ def post(self, request, *args, **kwargs): password = serializer.validated_data['password'] token = serializer.validated_data['token'] - # get token validation time - password_reset_token_validation_time = get_password_reset_token_expiry_time() - # find token reset_password_token = ResetPasswordToken.objects.filter(key=token).first() - if reset_password_token is None: - return Response({'status': 'notfound'}, status=status.HTTP_404_NOT_FOUND) - - # check expiry date - expiry_date = reset_password_token.created_at + timedelta(hours=password_reset_token_validation_time) - - if timezone.now() > expiry_date: - # delete expired token - reset_password_token.delete() - return Response({'status': 'expired'}, status=status.HTTP_404_NOT_FOUND) - # change users password (if we got to this code it means that the user is_active) if reset_password_token.user.eligible_for_reset(): pre_password_reset.send(sender=self.__class__, user=reset_password_token.user) From a258ade69f4d30d3413d1ae9aaee7b83b4172940 Mon Sep 17 00:00:00 2001 From: bomba1990 Date: Thu, 20 Feb 2020 16:20:01 -0300 Subject: [PATCH 02/13] message changes --- django_rest_passwordreset/serializers.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/django_rest_passwordreset/serializers.py b/django_rest_passwordreset/serializers.py index 935e36a..5824fbf 100644 --- a/django_rest_passwordreset/serializers.py +++ b/django_rest_passwordreset/serializers.py @@ -1,12 +1,13 @@ from datetime import timedelta +from django.core.exceptions import ValidationError from django.http import Http404 -from django.utils.translation import ugettext_lazy as _ -from django_rest_passwordreset.models import get_password_reset_token_expiry_time +from django.shortcuts import get_object_or_404 as _get_object_or_404 from django.utils import timezone - +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from rest_framework.generics import get_object_or_404 + +from django_rest_passwordreset.models import get_password_reset_token_expiry_time from . import models __all__ = [ @@ -28,7 +29,10 @@ def validate(self, data): password_reset_token_validation_time = get_password_reset_token_expiry_time() # find token - reset_password_token = get_object_or_404(models.ResetPasswordToken, key=token) + try: + reset_password_token = _get_object_or_404(models.ResetPasswordToken, key=token) + except (TypeError, ValueError, ValidationError): + raise Http404(_("The OTP password entered is not valid. Please check and try again.")) # check expiry date expiry_date = reset_password_token.created_at + timedelta( From 66dedc1333ad45129095a4a855397331958b9bfb Mon Sep 17 00:00:00 2001 From: bomba1990 Date: Thu, 20 Feb 2020 16:41:48 -0300 Subject: [PATCH 03/13] message changes --- django_rest_passwordreset/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index e22d50d..2d5139b 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -127,7 +127,7 @@ def post(self, request, *args, **kwargs): if not active_user_found and not getattr(settings, 'DJANGO_REST_PASSWORDRESET_NO_INFORMATION_LEAKAGE', False): raise exceptions.ValidationError({ 'email': [_( - "There is no active user associated with this e-mail address or the password can not be changed")], + "We couldn't find an account associate with that email. Please enter another email account.")], }) # last but not least: iterate over all users that are active and can change their password From dfdfa1060f0c84631cb654a8835e85c535322321 Mon Sep 17 00:00:00 2001 From: bomba1990 Date: Mon, 9 Mar 2020 12:39:28 -0300 Subject: [PATCH 04/13] reset message --- django_rest_passwordreset/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django_rest_passwordreset/serializers.py b/django_rest_passwordreset/serializers.py index 5824fbf..186afcb 100644 --- a/django_rest_passwordreset/serializers.py +++ b/django_rest_passwordreset/serializers.py @@ -31,7 +31,8 @@ def validate(self, data): # find token try: reset_password_token = _get_object_or_404(models.ResetPasswordToken, key=token) - except (TypeError, ValueError, ValidationError): + except (TypeError, ValueError, ValidationError, Http404, + models.ResetPasswordToken.DoesNotExist): raise Http404(_("The OTP password entered is not valid. Please check and try again.")) # check expiry date From 7b1398c45a0bb9825c35455fe351dc1be014e384 Mon Sep 17 00:00:00 2001 From: bomba1990 Date: Mon, 9 Mar 2020 16:18:00 -0300 Subject: [PATCH 05/13] validate_password only one message --- django_rest_passwordreset/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index 2d5139b..8d959c3 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -73,7 +73,7 @@ def post(self, request, *args, **kwargs): except ValidationError as e: # raise a validation error for the serializer raise exceptions.ValidationError({ - 'password': e.messages + 'password': e.messages[0] }) reset_password_token.user.set_password(password) From 87398f61d4ba5ebb1f1bc57f0cfd2c06c22c576d Mon Sep 17 00:00:00 2001 From: bomba1990 Date: Mon, 9 Mar 2020 16:20:13 -0300 Subject: [PATCH 06/13] validate_password only one message --- django_rest_passwordreset/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index 8d959c3..745a31b 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -73,7 +73,7 @@ def post(self, request, *args, **kwargs): except ValidationError as e: # raise a validation error for the serializer raise exceptions.ValidationError({ - 'password': e.messages[0] + 'password': list(e.messages)[0] }) reset_password_token.user.set_password(password) From df02c213bbdafc8cfef6eed1012e6fb98e821339 Mon Sep 17 00:00:00 2001 From: bomba1990 Date: Tue, 10 Mar 2020 18:16:50 -0300 Subject: [PATCH 07/13] Message for password error :( --- django_rest_passwordreset/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index 745a31b..f85e048 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -73,7 +73,7 @@ def post(self, request, *args, **kwargs): except ValidationError as e: # raise a validation error for the serializer raise exceptions.ValidationError({ - 'password': list(e.messages)[0] + 'password': _("Your password must contain at least 8 characters, one number and has a mix of uppercase and lowercase letters") }) reset_password_token.user.set_password(password) From fa03bd61bc3a8b05135cf95f085d0a998306029e Mon Sep 17 00:00:00 2001 From: Mariano ramirez Date: Wed, 25 Mar 2020 20:30:14 -0300 Subject: [PATCH 08/13] Message undo --- django_rest_passwordreset/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index f85e048..745a31b 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -73,7 +73,7 @@ def post(self, request, *args, **kwargs): except ValidationError as e: # raise a validation error for the serializer raise exceptions.ValidationError({ - 'password': _("Your password must contain at least 8 characters, one number and has a mix of uppercase and lowercase letters") + 'password': list(e.messages)[0] }) reset_password_token.user.set_password(password) From 549ddca5cd9d191eaff51e66493e84577e1fe74b Mon Sep 17 00:00:00 2001 From: Mariano ramirez Date: Wed, 25 Mar 2020 20:57:32 -0300 Subject: [PATCH 09/13] post_password_reset additional parameters --- django_rest_passwordreset/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index 745a31b..1379112 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -78,7 +78,8 @@ def post(self, request, *args, **kwargs): reset_password_token.user.set_password(password) reset_password_token.user.save() - post_password_reset.send(sender=self.__class__, user=reset_password_token.user) + post_password_reset.send(sender=self.__class__, user=reset_password_token.user, token=reset_password_token, + request=request) # Delete all password reset tokens for this user ResetPasswordToken.objects.filter(user=reset_password_token.user).delete() From 015763e092cfec855c67176808c4d400b4b812cd Mon Sep 17 00:00:00 2001 From: Mariano ramirez Date: Tue, 31 Mar 2020 19:36:16 -0300 Subject: [PATCH 10/13] Force request new token every time --- django_rest_passwordreset/views.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index 1379112..cd3cd34 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -139,16 +139,16 @@ def post(self, request, *args, **kwargs): token = None # check if the user already has a token - if user.password_reset_tokens.all().count() > 0: - # yes, already has a token, re-use this token - token = user.password_reset_tokens.all()[0] - else: +# if user.password_reset_tokens.all().count() > 0: +# # yes, already has a token, re-use this token +# token = user.password_reset_tokens.all()[0] +# else: # no token exists, generate a new token - token = ResetPasswordToken.objects.create( - user=user, - user_agent=request.META.get(HTTP_USER_AGENT_HEADER, ''), - ip_address=request.META.get(HTTP_IP_ADDRESS_HEADER, ''), - ) + token = ResetPasswordToken.objects.create( + user=user, + user_agent=request.META.get(HTTP_USER_AGENT_HEADER, ''), + ip_address=request.META.get(HTTP_IP_ADDRESS_HEADER, ''), + ) # send a signal that the password token was created # let whoever receives this signal handle sending the email for the password reset reset_password_token_created.send(sender=self.__class__, instance=self, reset_password_token=token) From f8bcd185797401913636c8b3fbe12f23b0db5ca0 Mon Sep 17 00:00:00 2001 From: Mariano ramirez Date: Wed, 1 Apr 2020 08:44:35 -0300 Subject: [PATCH 11/13] Delete old tokens --- django_rest_passwordreset/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index cd3cd34..68e438e 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -144,6 +144,7 @@ def post(self, request, *args, **kwargs): # token = user.password_reset_tokens.all()[0] # else: # no token exists, generate a new token + token = user.password_reset_tokens.delete() token = ResetPasswordToken.objects.create( user=user, user_agent=request.META.get(HTTP_USER_AGENT_HEADER, ''), From 59fa841b543418900f3bc647bf1efd9158dd0268 Mon Sep 17 00:00:00 2001 From: Mariano ramirez Date: Wed, 1 Apr 2020 09:07:59 -0300 Subject: [PATCH 12/13] ALl delete --- django_rest_passwordreset/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index 68e438e..bd37863 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -144,7 +144,7 @@ def post(self, request, *args, **kwargs): # token = user.password_reset_tokens.all()[0] # else: # no token exists, generate a new token - token = user.password_reset_tokens.delete() + token = user.password_reset_tokens.all().delete() token = ResetPasswordToken.objects.create( user=user, user_agent=request.META.get(HTTP_USER_AGENT_HEADER, ''), From 9cd14aa81e944d57ca5165aff2e11c7ed9d4d600 Mon Sep 17 00:00:00 2001 From: bomba1990 Date: Fri, 12 Jun 2020 17:14:51 -0300 Subject: [PATCH 13/13] Is_phone support --- django_rest_passwordreset/serializers.py | 17 ++++++++++++++++- django_rest_passwordreset/views.py | 13 +++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/django_rest_passwordreset/serializers.py b/django_rest_passwordreset/serializers.py index 186afcb..b0ed76f 100644 --- a/django_rest_passwordreset/serializers.py +++ b/django_rest_passwordreset/serializers.py @@ -1,10 +1,12 @@ from datetime import timedelta from django.core.exceptions import ValidationError +from django.core.validators import EmailValidator from django.http import Http404 from django.shortcuts import get_object_or_404 as _get_object_or_404 from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from phonenumber_field.phonenumber import to_python from rest_framework import serializers from django_rest_passwordreset.models import get_password_reset_token_expiry_time @@ -18,7 +20,20 @@ class EmailSerializer(serializers.Serializer): - email = serializers.EmailField() + email = serializers.CharField() + + def validate_email(self, value): + phone_number = to_python(value) + if phone_number and phone_number.is_valid(): + return value + + try: + validator = EmailValidator() + validator(value) + return value + except ValidationError: + raise ValidationError(_('Enter a valid phone number or email address.')) + class PasswordValidateMixin: diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index bd37863..4abaf0e 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -5,6 +5,8 @@ from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.conf import settings + +from phonenumber_field.phonenumber import to_python from rest_framework import status, exceptions from rest_framework.generics import GenericAPIView from rest_framework.response import Response @@ -102,6 +104,9 @@ def post(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'] + phone_number = to_python(email) + is_phone = phone_number and phone_number.is_valid() + # before we continue, delete all existing expired tokens password_reset_token_validation_time = get_password_reset_token_expiry_time() @@ -112,7 +117,10 @@ def post(self, request, *args, **kwargs): clear_expired(now_minus_expiry_time) # find a user by email address (case insensitive search) - users = User.objects.filter(**{'{}__iexact'.format(get_password_reset_lookup_field()): email}) + if is_phone: + users = User.objects.filter(**{'profile__mobile_phone_number__iexact': email}) + else: + users = User.objects.filter(**{'{}__iexact'.format(get_password_reset_lookup_field()): email}) active_user_found = False @@ -152,7 +160,8 @@ def post(self, request, *args, **kwargs): ) # send a signal that the password token was created # let whoever receives this signal handle sending the email for the password reset - reset_password_token_created.send(sender=self.__class__, instance=self, reset_password_token=token) + reset_password_token_created.send(sender=self.__class__, instance=self, reset_password_token=token, + is_phone=is_phone) # done return Response({'status': 'OK'})