Skip to content

Commit

Permalink
Merge 729b6bd into b608526
Browse files Browse the repository at this point in the history
  • Loading branch information
actlikewill committed Dec 5, 2018
2 parents b608526 + 729b6bd commit 9b5c18a
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 2 deletions.
9 changes: 9 additions & 0 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,12 @@ def update(self, instance, validated_data):

return instance


class ResetPasswordRequestSerializer(RegistrationSerializer):
# We have subclassed the RegistrationSerializer class to reuse the validation
# regex constraints
class Meta:
model = User
fields = ('password',)


50 changes: 50 additions & 0 deletions authors/apps/authentication/tests/test_password_reset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import datetime
import jwt
from django.conf import settings
from django.urls import reverse
from rest_framework import status
from authors.base_file import BaseTestCase


class TestPasswordReset(BaseTestCase):
# Test class for the password reset feature

def test_request_password_reset_with_valid_email(self):
self.client.post(self.register_url, self.register_data, format="json")
data = {"email": self.register_data['user']['email']}
response = self.client.post(reverse('authentication:forgot_password'), data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_request_password_reset_with_invalid_email(self):
data = {"email": "email@doesnotexist.com"}
response = self.client.post(reverse('authentication:forgot_password'), data, format="json")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_change_password_valid_password(self):
self.client.post(self.register_url, self.register_data, format="json")
token = jwt.encode({
"email": self.register_data['user']['email'],
"iat": datetime.datetime.now(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24)
}, settings.SECRET_KEY, algorithm='HS256').decode()
data = {"password": "password!@1"}
response = self.client.put(reverse("authentication:change_password", kwargs={'token': token}),
data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_change_password_invalid_password(self):
self.client.post(self.register_url, self.register_data, format="json")
token = jwt.encode({
"email": self.register_data['user']['email'],
"iat": datetime.datetime.now(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24)
}, settings.SECRET_KEY, algorithm='HS256').decode()
data = {"password": "password"}
response = self.client.put(reverse("authentication:change_password", kwargs={'token': token}),
data, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)





6 changes: 5 additions & 1 deletion authors/apps/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from django.urls import path

from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView, ForgotPasswordView,
ChangePasswordView
)

urlpatterns = [

path('user/', UserRetrieveUpdateAPIView.as_view(), name="user-retrieve-profile"),
path('users/', RegistrationAPIView.as_view(), name="user-signup"),
path('users/login/', LoginAPIView.as_view(), name="user-login"),
path('users/forgot_password/', ForgotPasswordView.as_view(), name="forgot_password"),
path('users/change_password/<str:token>', ChangePasswordView.as_view(), name="change_password")


]
77 changes: 76 additions & 1 deletion authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import jwt, datetime
from django.urls import reverse
from django.conf import settings
from django.core.mail import send_mail
from rest_framework import status
from rest_framework.generics import RetrieveUpdateAPIView, CreateAPIView
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import User

from .renderers import UserJSONRenderer
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer
LoginSerializer, RegistrationSerializer, UserSerializer, ResetPasswordRequestSerializer,
)


Expand Down Expand Up @@ -73,3 +78,73 @@ def update(self, request, *args, **kwargs):

return Response(serializer.data, status=status.HTTP_200_OK)


class ForgotPasswordView(APIView):
# This view handles sending the password reset request email.
# We expect the user to enter an email that exists in the database
# If no user is found a DoesNotExist exception is thrown
# We generate a token for the link because the one generated by the
# user models expires quickly. This one expires after 24 hrs
permission_classes = (AllowAny,)
renderer_classes = (UserJSONRenderer,)

def post(self, request):
try:
requester_data = request.data.get('email')
user = User.objects.get(email=requester_data)
token = jwt.encode({
"email": user.email,
"iat": datetime.datetime.now(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24)
}, settings.SECRET_KEY, algorithm='HS256').decode()

if request.is_secure():
protocol = "https://"
else:
protocol = "http://"
host = request.get_host()
path = reverse("authentication:change_password", kwargs={'token': token})
url = protocol + host + path
message = """
Click on the link to reset your password.
{}
""".format(url)
send_mail(
"Password Resetting Link",
message,
settings.EMAIL_HOST_USER,
[user.email],
fail_silently=True
)
success_message = {"success": "An email has been sent to your inbox with a password reset link."}
return Response(success_message, status=status.HTTP_200_OK)
except (KeyError, User.DoesNotExist):
error_message = {
"error": "That email does not exist."
}
return Response(error_message, status=status.HTTP_404_NOT_FOUND)


class ChangePasswordView(APIView):
# This is the view that changes the password.
# We use a put method. The link that is generated by the Forgot Password view has
# a token that is decoded here.
# We use the serializer to check if the password meets the requirements.
# We then call the set_password method to create the password and then save.
permission_classes = (AllowAny,)
serializer_class = ResetPasswordRequestSerializer

def put(self, request, token, *args, **kwargs):
try:
new_password = request.data.get('password')
serializer = self.serializer_class(data={"password": new_password})
serializer.is_valid(raise_exception=True)
decode_token = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256")
email = decode_token.get('email')
user = User.objects.get(email=email)
user.set_password(new_password)
user.save()
return Response({"Success": "Your password has been reset"}, status=status.HTTP_201_CREATED)

except jwt.PyJWTError:
return Response({"Error": "Invalid token. Please request a new password reset link."}, status=403)
6 changes: 6 additions & 0 deletions authors/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,9 @@
}
}
}
# These settings may have to be passed as environment settings and removed from here
EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'no.reply.technocrats@gmail.com'
EMAIL_HOST_PASSWORD = 'technocrats123'
EMAIL_PORT = 587

0 comments on commit 9b5c18a

Please sign in to comment.