Skip to content

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

Closed
wants to merge 13 commits into from
57 changes: 53 additions & 4 deletions django_rest_passwordreset/serializers.py
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',
Expand All @@ -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.'))
Comment on lines +25 to +35
Copy link
Contributor

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)




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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks much cleaner than the functionality in the post-method.

74 changes: 26 additions & 48 deletions django_rest_passwordreset/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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'})


Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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()

Expand All @@ -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

Expand All @@ -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.")],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be "We couldn't find an account associated with that email. Please try a different e-mail address."

})

# last but not least: iterate over all users that are active and can change their password
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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'})

Expand Down