Skip to content

Commit

Permalink
Merge d6fcb95 into fa2d61f
Browse files Browse the repository at this point in the history
  • Loading branch information
DrKimpatrick committed Sep 9, 2018
2 parents fa2d61f + d6fcb95 commit 8cee964
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,5 @@ sims1
db.sqlite3
.vscode/
*cache*
tmp
my_env
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ script:
- coveralls

after_script:
#- coveralls
#- coveralls
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ Example request body:

Authentication required, returns the User

Accepted fields: `email``username``password``image``bio`
Accepted fields: `email``username``password`

### Get Profile

Expand All @@ -254,6 +254,41 @@ Authentication required, returns a Profile

No additional parameters required

## Invoke a password reset

`POST /api/users/reset/password`

Example request body:

```source-json
{
"user":{
"email": "jake@jake.jake"
}
}
```
No authentication required, sends a password reset link to the email

Accepted fields: `email`


## Reset password

`PATCH /api/user/reset-password/<token>`

Example request body:

```source-json
{
"user":{
"password": "new_password"
}
}
```
Authentication required, returns the User

Accepted fields: `email``username``password`

### List Articles

`GET /api/articles`
Expand Down
34 changes: 34 additions & 0 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import re
from .models import User

import os

from django.shortcuts import get_object_or_404


class RegistrationSerializer(serializers.ModelSerializer):
"""Serializers registration requests and creates a new user."""
Expand Down Expand Up @@ -182,3 +186,33 @@ def update(self, instance, validated_data):
instance.save()

return instance

class InvokePasswordReset(serializers.Serializer):

email = serializers.CharField(max_length=255)

def validate(self, data):
email = data.get('email', None)

# An email is required.
if email is None:
raise serializers.ValidationError(
'An email address is required.'
)

user = get_object_or_404(User, email=email)

# get user token
token = user.token

if user is None:
raise serializers.ValidationError(
'A user with this email was not found.'
)

# call send email method here
# email_structure(email, token)

return {
'email': token
}
9 changes: 8 additions & 1 deletion authors/apps/authentication/tests/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,16 @@ def __init__(self):
}
}


self.no_password_login = {
"user": {"email": self.user_email,
"password": None,
}
}



self.invoke_email = {
"user": {
"email": self.user_email
}
}
73 changes: 73 additions & 0 deletions authors/apps/authentication/tests/test_password_reset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""This module tests the password reset functionality of a user."""
from django.test import TestCase

from rest_framework.test import APIClient
from rest_framework import status
from authors.apps.authentication.tests.base_test import BaseTest
from authors.apps.authentication.models import User


class PasswordResetTestCase(TestCase, BaseTest):
""" This class defines the test suite for password reset """

def setUp(self):
BaseTest.__init__(self)
self.client = APIClient()

# create a new user
self.user = User.objects.create_user(
self.user_name, self.user_email, self.password
)

# invoke a password reset email
self.response = self.client.post("/api/users/reset/password",
self.invoke_email,
format="json")

# This email is not associated with a user
self.email_404 = {
"user": {
"email": "email_404@none.com"
}
}

# Here we don't pass in an email
self.empty_email = {
"user": {
"email": ""
}
}

def test_invoke_password_reset(self):
""" Test that password reset email is sent
The user with this email exists
"""

self.assertEqual(self.response.status_code, status.HTTP_200_OK)

def test_email_does_not_exist(self):
""" Test user can not invoke password reset for email that does not exist"""

self.response = self.client.post(
"/api/users/reset/password",
self.email_404,
format="json"

)

self.assertEqual(self.response.status_code, status.HTTP_404_NOT_FOUND)

def test_with_no_email(self):
""" Test a user can not invoke password reset with no email """

self.response = self.client.post(
"/api/users/reset/password",
self.empty_email,
format="json"
)

self.assertEqual(self.response.status_code, status.HTTP_400_BAD_REQUEST)

self.assertEqual(self.response.json()['errors'],
{'email': ['This field may not be blank.']})

22 changes: 22 additions & 0 deletions authors/apps/authentication/tests/test_retrieve_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def setUp(self):
self.user_get_response = self.client.get("/api/user/")
self.user_put_response = self.client.put("/api/user/")


def test_non_authorized_user_blocked(self):
"""Test the api can block a non authorized"""

Expand Down Expand Up @@ -124,3 +125,24 @@ def test_a_deactivated_user(self):
self.response = self.client.get("/api/user/")
self.assertEqual('This user has been deactivated.',
self.response.data['detail'])

# Test update user info
def test_update_user_password(self):
"""test update user """

token = self.login_response.data['token']
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token)

self.update_user = self.client.put("/api/user/",

data={
"user": {
"username": self.user_name,
"email": self.user_email,
"password": self.password
}
}
,
format="json")
self.assertEqual(self.update_user.status_code, status.HTTP_200_OK)

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

from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView, InvokePasswordResetAPIView
)

urlpatterns = [
path('user/', UserRetrieveUpdateAPIView.as_view()),
path('users/', RegistrationAPIView.as_view()),
path('users/login/', LoginAPIView.as_view()),
path('users/reset/password', InvokePasswordResetAPIView.as_view()),
path('user/reset-password/<token>', UserRetrieveUpdateAPIView.as_view())
]
22 changes: 22 additions & 0 deletions authors/apps/authentication/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import os
from django.core.mail import send_mail

def send_password_reset_email(to_email, token, current_site):

# current_site = os.environ.get("CURRENT_SITE")
# email setup
subject = 'Password reset'
message = """
You're receiving this email because you invoked a password reset on
Authors haven. If you think it is a mistake to receive this email just ignore it.
----- Click the link below to reset your password ----
{}/api/user/reset-password/{}
""".format(current_site, token)
from_email = os.environ.get('EMAIL_HOST_USER')
to_email = to_email
try:
send_mail(subject, message, from_email, [to_email], fail_silently=False,)
except Exception as e:
return {'email': str(e)}
38 changes: 35 additions & 3 deletions authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

from .renderers import UserJSONRenderer
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer
LoginSerializer, RegistrationSerializer, UserSerializer, InvokePasswordReset
)
import os
from .utils import send_password_reset_email


class RegistrationAPIView(APIView):
Expand Down Expand Up @@ -43,10 +45,8 @@ def post(self, request):
# handles everything we need.
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)

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


class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
renderer_classes = (UserJSONRenderer,)
Expand All @@ -73,3 +73,35 @@ def update(self, request, *args, **kwargs):

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


class InvokePasswordResetAPIView(LoginAPIView):
"""
This view allows the user to invoke a password reset email
It inherits post method of the LoginAPIView
"""
permission_classes = (AllowAny,)
serializer_class = InvokePasswordReset


def post(self, request):
user = request.data.get('user', {})

# get current site
if 'HTTP_HOST' in request.META:
current_site = request.META['HTTP_HOST']
current_site = "https://{}".format(current_site)
else:
current_site = "http://127.0.0.1:8000"

serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)

# call send email function
send_password_reset_email(user['email'], serializer.data['email'], current_site)

return Response({"message": "Check your email for a link"}, status=status.HTTP_200_OK)





9 changes: 9 additions & 0 deletions authors/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,12 @@
),

}

# Email send configurations
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_PORT = 587
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = True

10 changes: 7 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@

atomicwrites==1.2.0
attrs==18.1.0
astroid==2.0.4
coverage==4.5.1
dj-database-url==0.5.0
Django==2.1
Expand All @@ -17,13 +19,15 @@ gunicorn==19.9.0
more-itertools==4.3.0
nose==1.3.7
pluggy==0.7.1
isort==4.3.4
lazy-object-proxy==1.3.1
mccabe==0.6.1
psycopg2==2.7.5
psycopg2-binary==2.7.5
py==1.6.0
PyJWT==1.6.4
pytest==3.7.3
pytest-django==3.4.2
pylint==2.1.1
pytz==2018.5
requests==2.19.1
requests-oauthlib==1.0.0
six==1.11.0
wrapt==1.10.11

0 comments on commit 8cee964

Please sign in to comment.