Skip to content

Commit

Permalink
Merge fc91e3b into ee83ee9
Browse files Browse the repository at this point in the history
  • Loading branch information
pbkabali committed Feb 6, 2019
2 parents ee83ee9 + fc91e3b commit f771a43
Show file tree
Hide file tree
Showing 12 changed files with 363 additions and 13 deletions.
6 changes: 6 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
},
"SECRET_KEY": {
"required": true
},
"USER_EMAIL": {
"required": true
},
"USER_PASSWORD": {
"required": true
}
},
"formation": {},
Expand Down
21 changes: 21 additions & 0 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,24 @@ class TwitterAuthSerializer(serializers.Serializer):
class GoogleFacebookAuthSerializer(serializers.Serializer):
"""Handle serialization of Google and Facebook access tokens"""
access_token = serializers.CharField(max_length=2000)

class PasswordResetRequestSerializer(serializers.Serializer):
email = serializers.EmailField(max_length=255, required=True)

def validate(self,data):
email = data.get('email', None)
if email is None:
raise serializers.ValidationError("Email field is required")
return email

class PasswordResetSerializer(serializers.Serializer):
new_password = serializers.CharField(
max_length=128, allow_blank=False, write_only=True, required=True,
min_length=6
)
def validate(self, data):
new_password = data.get('new_password', None)
if not re.compile(r'^[0-9a-zA-Z]*$').match(new_password):
raise ValidationError(
"Password must contain alphanumeric characters only")
return new_password
2 changes: 1 addition & 1 deletion authors/apps/authentication/tests/base_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def setUp(self):

self.url_register = reverse('authentication:register')
self.url_login = reverse('authentication:login')
self.url_user_detail = reverse('authentication:retrieve_update')
self.url_user_detail = reverse('authentication:retrieve-update')
self.url_twitter = reverse('authentication:twitter-auth')
self.url_google = reverse('authentication:google-auth')
self.url_facebook = reverse('authentication:facebook-auth')
Expand Down
51 changes: 51 additions & 0 deletions authors/apps/authentication/tests/test_data/password_reset_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Test data for password reset feature"""
import jwt
from django.conf import settings

from .login_data import valid_login_data

registered_email = {
"user": {
"email": valid_login_data["user"]["email"]
}
}

unregistered_email = {
"user": {
"email": "jklm@mno.org"
}
}

new_valid_password = {
"user": {
"new_password": "xml123XML"
}
}

new_short_password = {
"user": {
"new_password": "ab2"
}
}

new_invalid_password = {
"user": {
"new_password": "abcd@efgh"
}
}

new_blank_password = {
"user": {
"new_passord":""
}
}

token = jwt.encode({
"email":valid_login_data["user"]["email"],
},
settings.SECRET_KEY,
algorithm='HS256'
)
reset_link = '/api/v1/users/reset-password/'+ token.decode('utf-8') + '/'

invalid_reset_link = '/api/v1/users/reset-password/'+ token.decode('utf-8') + 'wrongL!NK/'
109 changes: 109 additions & 0 deletions authors/apps/authentication/tests/test_reset_password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
This file contains tests for the password reset feature
where the user who forgets their password can reset it
via a verification link sent directly into their registerd
mailbox.
"""
from .base_class import BaseTest
import json
from rest_framework import status
from .test_data.password_reset_data import (reset_link, invalid_reset_link,
registered_email, unregistered_email, new_valid_password,
new_blank_password, new_invalid_password, new_short_password
)
from django.urls import reverse


class ResetPassword(BaseTest):

def setUp(self):
super().setUp()
self.register_test_user()
self.password_reset_request_url = reverse(
'authentication:request-password-reset'
)

def test_request_password_reset_registered_user(self):
"""test user with valid account requests password reset"""
response = self.client.post(
self.password_reset_request_url,
data=json.dumps(
registered_email
),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_request_password_reset_unregistered_user(self):
"""test user with no account requests for a password reset"""
response = self.client.post(
self.password_reset_request_url,
data=json.dumps(
unregistered_email
),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_request_password_reset_valid_link(self):
"""
test user uses valid reset link sent to their email
to access password reset endpoint
"""
response = self.client.get(
reset_link,
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_request_password_reset_invalid_link(self):
"""test user uses invalid reset link to access password reset endpoint"""
response = self.client.get(
invalid_reset_link,
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_password_reset_valid_password(self):
"""test user tries to reset to a valid password"""
response = self.client.post(
reset_link,
data=json.dumps(
new_valid_password
),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_password_reset_short_password(self):
"""test user tries to supply an undesirable short password"""
response = self.client.post(
reset_link,
data=json.dumps(
new_short_password
),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_password_reset_invalid_password(self):
"""test user tries to supply a password with forbidden characters"""
response = self.client.post(
reset_link,
data=json.dumps(
new_invalid_password
),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_password_reset_blank_password(self):
"""test user tries to supply an empty password"""
response = self.client.post(
reset_link,
data=json.dumps(
new_blank_password
),
content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
2 changes: 1 addition & 1 deletion authors/apps/authentication/tests/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class UpdateTest(BaseTest):

def setUp(self):
super().setUp()
self.url_update = reverse('authentication:retrieve_update')
self.url_update = reverse('authentication:retrieve-update')

def test_user_can_successfully_update_email(self):
"""Test that an existing user can successfully update their email"""
Expand Down
13 changes: 11 additions & 2 deletions authors/apps/authentication/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@

from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView,
TwitterAuthAPIView, GoogleAuthAPIView, FacebookAuthAPIView
TwitterAuthAPIView, GoogleAuthAPIView, FacebookAuthAPIView,
PasswordResetRequestAPIView, PasswordResetAPIView
)
app_name = "authentication"
urlpatterns = [
path('user/', UserRetrieveUpdateAPIView.as_view(), name='retrieve_update'),
path('user/', UserRetrieveUpdateAPIView.as_view(), name='retrieve-update'),
path('users/', RegistrationAPIView.as_view(), name='register'),
path('users/login/', LoginAPIView.as_view(), name='login'),
path('users/login/twitter/', TwitterAuthAPIView.as_view(), name='twitter-auth'),
path('users/login/google/', GoogleAuthAPIView.as_view(), name='google-auth'),
path('users/login/facebook/', FacebookAuthAPIView.as_view(), name='facebook-auth'),
path(
'users/request-password-reset/', PasswordResetRequestAPIView.as_view(),
name='request-password-reset'
),
path(
'users/reset-password/<token>/', PasswordResetAPIView.as_view(),
name='reset-password'
),
]
74 changes: 65 additions & 9 deletions authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from ..core.password_reset_manager import PasswordResetManager

from .renderers import UserJSONRenderer
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer,
PasswordResetRequestSerializer, PasswordResetSerializer,
TwitterAuthSerializer, GoogleFacebookAuthSerializer
)

import os
import twitter

import facebook

from google.oauth2 import id_token
from google.auth.transport import requests

from .social_login import(login_or_register_social_user)

from .models import User

class RegistrationAPIView(GenericAPIView):
# Allow any user (authenticated or not) to hit this endpoint.
Expand Down Expand Up @@ -59,7 +57,6 @@ def post(self, request):

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


class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
renderer_classes = (UserJSONRenderer,)
Expand Down Expand Up @@ -87,7 +84,6 @@ def update(self, request, *args, **kwargs):

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


class TwitterAuthAPIView(GenericAPIView):
"""
Handle login of a Twitter user via the Twitter Api.
Expand Down Expand Up @@ -127,7 +123,6 @@ def post(self, request):

return login_or_register_social_user(twitter_user)


class GoogleAuthAPIView(GenericAPIView):
"""
Handle login of a Google user via the Google oauth2.
Expand Down Expand Up @@ -157,7 +152,6 @@ def post(self, request):

return login_or_register_social_user(google_user)


class FacebookAuthAPIView(GenericAPIView):
"""
Handle login of a Facebook user via the Facebook Graph API.
Expand All @@ -183,3 +177,65 @@ def post(self, request):
facebook_user = graph.get_object(id='me', fields='email, name')

return login_or_register_social_user(facebook_user)

class PasswordResetRequestAPIView(GenericAPIView):
"""
This handles receiving the requester's email address and sends
a password reset email
"""
permission_classes = (AllowAny,)
renderer_classes = (UserJSONRenderer,)
serializer_class = PasswordResetRequestSerializer

def post(self, request):
user = request.data.get(
'user', {}) if 'user' in request.data else request.data
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
user_email = user.get("email", None)
PasswordResetManager(request).send_password_reset_email(user_email)
return Response(
{
"message":"An email has been sent to your mailbox with instructions to reset your password",
},
status=status.HTTP_200_OK
)

class PasswordResetAPIView(GenericAPIView):
"""
This handles verification of reset link and updates password field
with new password
"""
permission_classes = (AllowAny,)
renderer_classes = (UserJSONRenderer,)
serializer_class = PasswordResetSerializer

def get(self, request, token):
user = PasswordResetManager(request).get_user_from_encoded_token(token)
if user is None:
return Response({"message":"Invalid token!"}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
"message":"Token is valid. OK to send new password information"
},
status=status.HTTP_200_OK
)

def post(self, request, token):
data = request.data.get(
'user', {}) if 'user' in request.data else request.data
print(data)
user = PasswordResetManager(request).get_user_from_encoded_token(token)
if user is None:
return Response(
{
"message":"Your token has expired. Please start afresh."
},
status=status.HTTP_400_BAD_REQUEST
)
email = user.get('email')
serializer = self.serializer_class(data=data)
serializer.is_valid(raise_exception=True)
new_password = data.get("new_password")
PasswordResetManager(request).update_password(email, new_password)
return Response({"message":"Your password has been reset"} , status=status.HTTP_200_OK)
Loading

0 comments on commit f771a43

Please sign in to comment.