-
Notifications
You must be signed in to change notification settings - Fork 150
Moved some validations to the serializer #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
59c41ae
a258ade
66dedc1
dfdfa10
7b1398c
87398f6
df02c21
fa03bd6
549ddca
015763e
f8bcd18
59fa841
9cd14aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,17 @@ | ||
from django.utils.translation import ugettext_lazy as _ | ||
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 | ||
from . import models | ||
|
||
__all__ = [ | ||
'EmailSerializer', | ||
'PasswordTokenSerializer', | ||
|
@@ -10,13 +20,52 @@ | |
|
||
|
||
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: | ||
def validate(self, data): | ||
token = data.get('token') | ||
|
||
class PasswordTokenSerializer(serializers.Serializer): | ||
# get token validation time | ||
password_reset_token_validation_time = get_password_reset_token_expiry_time() | ||
|
||
# find token | ||
try: | ||
reset_password_token = _get_object_or_404(models.ResetPasswordToken, key=token) | ||
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 | ||
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() | ||
|
||
Comment on lines
+43
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks much cleaner than the functionality in the post-method. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,9 @@ | |
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 phonenumber_field.phonenumber import to_python | ||
from rest_framework import status, exceptions | ||
from rest_framework.generics import GenericAPIView | ||
from rest_framework.response import Response | ||
|
||
|
@@ -40,25 +42,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 +59,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) | ||
|
@@ -106,12 +75,13 @@ def post(self, request, *args, **kwargs): | |
except ValidationError as e: | ||
# raise a validation error for the serializer | ||
raise exceptions.ValidationError({ | ||
'password': e.messages | ||
'password': list(e.messages)[0] | ||
}) | ||
|
||
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() | ||
|
@@ -134,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() | ||
|
||
|
@@ -144,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 | ||
|
||
|
@@ -160,7 +136,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.")], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be |
||
}) | ||
|
||
# last but not least: iterate over all users that are active and can change their password | ||
|
@@ -171,19 +147,21 @@ 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: | ||
Comment on lines
+150
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove obsolete code entirely |
||
# 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 = user.password_reset_tokens.all().delete() | ||
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) | ||
reset_password_token_created.send(sender=self.__class__, instance=self, reset_password_token=token, | ||
is_phone=is_phone) | ||
# done | ||
return Response({'status': 'OK'}) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conflicts with #93, which implements a universal lookup field (Not limited to phone/email)