Skip to content

Commit

Permalink
Merge pull request #23 from andela/ft-fix-password-reset-165305756
Browse files Browse the repository at this point in the history
#165305756 Fix Password Reset Feature
  • Loading branch information
Oluwagbenga Joloko committed May 5, 2019
2 parents 3a0387d + 4670354 commit 84dd001
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 155 deletions.
1 change: 0 additions & 1 deletion .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,3 @@ export EMAIL_HOST_PASSWORD='your email host password'
export EMAIL_PORT='your email port'
export EMAIL_HOST_USER='your email host user'
export EMAIL_USE_TLS=True
export DOMAIN_NAME='your domain name'
4 changes: 1 addition & 3 deletions authors/apps/authentication/renderers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json

from rest_framework.renderers import JSONRenderer


Expand All @@ -18,12 +17,12 @@ def render(self, data, media_type=None, renderer_context=None):
# rendering errors.
return super(UserJSONRenderer, self).render(data)


# Finally, we can render our data under the "user" namespace.
return json.dumps({
'user': data
})


class SignupUserJSONRenderer(JSONRenderer):
charset = 'utf-8'

Expand All @@ -39,7 +38,6 @@ def render(self, data, media_type=None, renderer_context=None):
# rendering errors.
return super(SignupUserJSONRenderer, self).render(data)


# Finally, we can render our data under the "user" namespace.
return json.dumps({
'user': data,
Expand Down
116 changes: 99 additions & 17 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from django.contrib.auth import authenticate

from rest_framework import serializers
from rest_framework.validators import UniqueValidator

from .models import User
from django.core.validators import EmailValidator
from django.core.exceptions import ValidationError
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from rest_framework.authtoken.models import Token
from django.contrib.auth.password_validation import validate_password
from password_strength import PasswordStats


class RegistrationSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -226,55 +225,138 @@ class SocialAuthSerializer(serializers.Serializer):
allow_blank=True,
default=""
)


class PasswordResetSerializer(serializers.Serializer):
"""
Serializer for password reset
"""
email = serializers.EmailField(required=True)
email = serializers.EmailField(
required=False,
allow_blank=True,
allow_null=True,
write_only=True
)

def validate_email(self, value):
def validate(self, data):
email = data.get("email")
if not email:
raise serializers.ValidationError({
"email": "Please provide your email address"
})
try:
EmailValidator(value)
EmailValidator(email)
except ValidationError as e:
raise serializers.ValidationError(e)
try:
User.objects.get(email=value)
User.objects.get(email=email)
except ObjectDoesNotExist:
raise serializers.ValidationError(
"no account with that email address"
)
raise serializers.ValidationError({
"email": "No account with that email address"
})
return data


class PasswordResetConfirmSerializer(serializers.Serializer):
"""
Serializer class for password reset confirm
"""
token = serializers.CharField(max_length=250, required=True)
password = serializers.CharField(max_length=250, required=True)
password_confirm = serializers.CharField(max_length=250, required=True)
token = serializers.CharField(
max_length=250,
write_only=True,
allow_null=True,
allow_blank=True,
required=False
)
password = serializers.CharField(
max_length=250,
write_only=True,
allow_null=True,
allow_blank=True,
required=False
)
password_confirm = serializers.CharField(
max_length=250,
write_only=True,
allow_null=True,
allow_blank=True,
required=False
)

def validate(self, data):
if not data.get("token"):
raise serializers.ValidationError({
"token": "There is no token provided"
})
try:
token_object = Token.objects.get(key=data.get("token"))
except ObjectDoesNotExist:
raise serializers.ValidationError({
"token": "invalid token"
"token": "Invalid token"
})
if self.expired_token(token_object):
raise serializers.ValidationError({
"token": "invalid token"
"token": "Expired token"
})
self.get_password_field_erros(data)
try:
validate_password(data.get("password"))
except ValidationError as error:
raise serializers.ValidationError({
"password": list(error)
})
if data["password"] != data["password_confirm"]:
self.get_password_policy_rrors(data.get("password"))
return data

def get_password_policy_rrors(self, password):
"""
Captures password policy errors
"""
stats = PasswordStats(password)
password_errors = []
if stats.letters_uppercase < 2:
password_errors.append(
"Your password should have a minimum of 2 uppercase letters"
)
if stats.numbers < 2:
password_errors.append(
"Your password should have a minimum of 2 numbers"
)
if stats.special_characters < 1:
password_errors.append(
"Your password should have a minimum of 1 special character"
)
elif stats.letters_lowercase < 3:
password_errors.append(
"Your password should have a minimum of 3 lowercase letters"
)
if password_errors:
raise serializers.ValidationError({
"password": list(password_errors)
})

def get_password_field_erros(self, data):
"""
Captures null, blank and empty fields for password
and password confirm
"""
if not data.get("password") and not data.get("password_confirm"):
raise serializers.ValidationError({
"password": "passwords did not match"
"password": "Please provide your password",
"password_confirm": "Please confirm your password"
})
elif not data.get("password"):
raise serializers.ValidationError({
"password": "Please provide your password"
})
elif not data.get("password_confirm"):
raise serializers.ValidationError({
"password_confirm": "Please confirm your password"
})
elif data["password"] != data["password_confirm"]:
raise serializers.ValidationError({
"password": "Passwords did not match"
})
return data

def expired_token(self, auth_token):
"""
Expand Down
68 changes: 22 additions & 46 deletions authors/apps/authentication/tests/basetests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rest_framework.test import APITestCase, APIClient
from rest_framework.reverse import reverse
from rest_framework.authtoken.models import Token
from django.utils import timezone

User = get_user_model()

Expand All @@ -30,15 +31,15 @@ def setUp(self):
email="adam@gmail.com",
password="@Us3r.com",
)
self.user.is_verified=True
self.user.is_verified = True
self.user.save()

self.user1 = User.objects.create_user(
username="ian",
email="ian@gmail.com",
password="Maina9176",
)
self.user1.is_verified=True
self.user1.is_verified = True
self.user1 = User.objects.get(username='ian')
self.user1.is_active = False
self.user1.save()
Expand All @@ -61,10 +62,9 @@ def setUp(self):
password="Maina9176",
)

self.user4.is_verified=True
self.user4.is_verified = True
self.user4.save()


self.super_user = User.objects.create_superuser(
username="admin",
email="admin@authors.com",
Expand Down Expand Up @@ -213,9 +213,8 @@ def setUp(self):
}
}
self.password_data = {
"token": self.token,
"password": "HenkDTestPAss!#",
"password_confirm": "HenkDTestPAss!#"
"password": "HenkDTestPAss23!#",
"password_confirm": "HenkDTestPAss23!#"
}
self.contains_error = lambda container, error: error in container

Expand All @@ -231,54 +230,31 @@ def password_reset(self):
)
token, created = Token.objects.get_or_create(user=self.user)
self.token = token.key
self.password_data["token"] = self.token
self.set_password_confirm_url()
return response

def password_reset_confirm(self):
def generate_fake_token(self):
"""
Confirms password reset by posting new password
Generates invalid token
"""
return self.client.post(social_url, social_user)

class PasswordResetBaseTest(BaseTest):
"""
Base test for testing passeord reset
"""
self.token = "fjeojfoeefubbfbwuebbyvyfvwd24"
self.set_password_confirm_url()

def setUp(self):
super().setUp()
self.client = APIClient()
self.email = self.user.email
self.password_reset_url = reverse("authentication:password_reset")
self.token = None
self.password_reset_confirm_url = reverse(
"authentication:password_reset_confirm")
self.reset_data = {
"user": {
"email": self.email
}
}
self.password_data = {
"token": self.token,
"password": "HenkDTestPAss!#",
"password_confirm": "HenkDTestPAss!#"
}
self.contains_error = lambda container, error: error in container

def password_reset(self):
def generate_expired_token(self):
"""
Verifies user account and generates reset password
token
Generates expired token
"""
response = self.client.post(
path=self.password_reset_url,
data=self.reset_data,
format="json"
)
token, created = Token.objects.get_or_create(user=self.user)
token.created = timezone.now()-timezone.timedelta(hours=25)
token.save()
self.token = token.key
self.password_data["token"] = self.token
return response
self.set_password_confirm_url()

def set_password_confirm_url(self):
"""
Sets password confirm url as the token changes
"""
self.password_reset_confirm_url += '?token='+self.token

def password_reset_confirm(self):
"""
Expand Down
Loading

0 comments on commit 84dd001

Please sign in to comment.