Skip to content

Commit

Permalink
feat(verification): implement email verification on successful user s…
Browse files Browse the repository at this point in the history
…ignup

- ensure an email is sent to a user once they sign up to be activated
- update requirements.txt
- update env-example
- change authentication urls to use path
- refactor and add tests
[Delivers #165305262]
  • Loading branch information
Alvin Mugambi authored and Alvin Mugambi committed Apr 25, 2019
1 parent 8c299ce commit ac3d5e6
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 28 deletions.
1 change: 1 addition & 0 deletions authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,4 @@ def get_short_name(self):
the user's real name, we return their username instead.
"""
return self.username

12 changes: 9 additions & 3 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .models import User

from django.core.validators import RegexValidator
from rest_framework.exceptions import NotAuthenticated


class RegistrationSerializer(serializers.ModelSerializer):
Expand All @@ -22,14 +23,16 @@ class RegistrationSerializer(serializers.ModelSerializer):
validators=[alphanumeric]
)

token = serializers.SerializerMethodField()

# The client should not be able to send a token along with a registration
# request. Making `token` read-only handles that for us.

class Meta:
model = User
# List all of the fields that could possibly be included in a request
# or response, including fields specified explicitly above.
fields = ['email', 'username', 'password']
fields = ['id', 'email', 'username', 'password', 'token']

def create(self, validated_data):
# Use the `create_user` method we wrote earlier to create a new user.
Expand All @@ -38,6 +41,8 @@ def create(self, validated_data):
user.save(update_fields=['is_active'])
return user

def get_token(self, token):
return default_token_generator.make_token(token)

class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255)
Expand Down Expand Up @@ -86,8 +91,8 @@ def validate(self, data):
# or otherwise deactivated. This will almost never be the case, but
# it is worth checking for. Raise an exception in this case.
if not user.is_active:
raise serializers.ValidationError(
'This user has been deactivated.'
raise NotAuthenticated(
'This user account is not active.'
)

# The `validate` method should return a dictionary of validated data.
Expand Down Expand Up @@ -184,3 +189,4 @@ def validate(self, attrs):
self.fail('invalid_token')

return attrs

4 changes: 3 additions & 1 deletion authors/apps/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework_swagger.views import get_swagger_view
from django.urls import path
from django.conf.urls import url

from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView, ActivationView
Expand All @@ -12,5 +13,6 @@
path('users/', RegistrationAPIView.as_view()),
path('users/login/', LoginAPIView.as_view()),
path('users/activate/', ActivationView.as_view()),
path('/', swagger_view)
url(r'^$', swagger_view)
]

8 changes: 1 addition & 7 deletions authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
<<<<<<< HEAD
from rest_framework import status
=======
from django.contrib.auth.tokens import default_token_generator
from django.utils.translation import ugettext_lazy as _

from rest_framework import status, generics, permissions, views
>>>>>>> 530963d614f1c9e9c18c60694031f17917966379
from rest_framework.generics import RetrieveUpdateAPIView, GenericAPIView
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
Expand Down Expand Up @@ -89,8 +85,6 @@ def update(self, request, *args, **kwargs):
serializer.save()

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

class ActivationView(generics.GenericAPIView):
"""
Expand Down Expand Up @@ -121,4 +115,4 @@ def post(self, request):
}

return Response(data=response_data ,status=status.HTTP_200_OK)
>>>>>>> 530963d614f1c9e9c18c60694031f17917966379

3 changes: 1 addition & 2 deletions authors/apps/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from rest_framework.views import exception_handler
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import APIException, NotFound
from rest_framework.exceptions import APIException
from rest_framework import status
from django.http import Http404

def core_exception_handler(exc, context):
# If an exception is thrown that we don't explicitly handle here, we want
Expand Down
9 changes: 3 additions & 6 deletions authors/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,8 @@
'authors.apps.core',
'authors.apps.profiles',
'rest_framework_swagger',
<<<<<<< HEAD
=======

'mailer'
>>>>>>> 530963d614f1c9e9c18c60694031f17917966379
]

MIDDLEWARE = [
Expand Down Expand Up @@ -156,6 +153,8 @@
# ),
}

AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Swagger Settings
SWAGGER_SETTINGS = {
Expand All @@ -170,12 +169,10 @@
}
}
}
<<<<<<< HEAD
=======
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
EMAIL_PORT = 587
EMAIL_USE_TLS = True
>>>>>>> 530963d614f1c9e9c18c60694031f17917966379

19 changes: 15 additions & 4 deletions authors/tests/data/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,22 @@ def signup_user(self, data=''):
data = data or self.base_data.user_data

return self.client.post(
"/api/users",
"/api/users/",
data,
format="json"
)

def activate_user(self, uid, token):
"""
This method 'activate_user' activates a user account
using the provided details
"""

return self.client.post(
"/api/users/activate/?uid={}&token={}".format(uid, token),
format="json"
)

def login_user(self, data=''):
"""
This method 'login_user'
Expand All @@ -40,7 +51,7 @@ def login_user(self, data=''):
data = data or self.base_data.login_data

response = self.client.post(
"/api/users/login",
"/api/users/login/",
data,
format="json"
)
Expand All @@ -55,7 +66,7 @@ def fetch_current_user(self, headers=""):
"""

return self.client.get(
"/api/user",
"/api/user/",
format="json"
)

Expand All @@ -69,7 +80,7 @@ def update_user_details(self, data=''):
data = data or self.base_data.update_data

return self.client.put(
"/api/user",
"/api/user/",
data,
format="json"
)
124 changes: 123 additions & 1 deletion authors/tests/data/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ def setUp(self):

BaseTest.setUp(self)

self.signup_user()
signup = self.signup_user()
uid = signup.data.get('id')
token = signup.data.get('token')

self.activate_user(uid=uid, token=token)

def test_successful_login_if_correct_credentials(self):
"""
Expand Down Expand Up @@ -80,3 +84,121 @@ def test_raises_error_if_incorrect_credentials(self):
"email and password was not found" in self.login_user(
).data["errors"]["error"][0]
)


def test_user_cannot_login_with_an_inactive_account(self):
"""
Test method to ensure users cannot login without an inactive
account
"""
data = {
"user": {
"email": "testuser2@email.com",
"username": "Alpha2",
"password": "Admin12345"
}
}

self.client.post(
"/api/users/",
data,
format="json"
)
login = self.client.post(
"/api/users/login/",
data,
format="json"
)

self.assertEqual(login.status_code,
status.HTTP_403_FORBIDDEN)


def test_user_cannot_activate_account_with_wrong_uid(self):
"""
Test method to ensure users cannot activate their account with
a wrong id
"""
data = {
"user": {
"email": "testuser2@email.com",
"username": "Alpha2",
"password": "Admin12345"
}
}

signup = self.client.post(
"/api/users/",
data,
format="json"
)
activate = self.client.post(
"/api/users/activate/?uid={a}&token={b}".format(a='abc',
b=signup.data.get('token')),
format="json"
)

self.assertEqual(activate.status_code,
status.HTTP_400_BAD_REQUEST)
self.assertEqual(activate.data.get('errors')['uid'][0],
'A valid integer is required.')


def test_user_cannot_activate_account_with_wrong_token(self):
"""
Test method to ensure users cannot activate their account with
a wrong token
"""
data = {
"user": {
"email": "testuser2@email.com",
"username": "Alpha2",
"password": "Admin12345"
}
}

signup = self.client.post(
"/api/users/",
data,
format="json"
)
activate = self.client.post(
"/api/users/activate/?uid={a}&token={b}".format(a=signup.data.get('id'),
b='12345'),
format="json"
)

self.assertEqual(activate.status_code,
status.HTTP_400_BAD_REQUEST)
self.assertEqual(activate.data.get('errors')['error'][0],
'The provided token for the user is not valid.')


def test_user_can_activate_account_with_right_credentials(self):
"""
Test method to ensure users can activate their account with
right credentials
"""
data = {
"user": {
"email": "testuser2@email.com",
"username": "Alpha2",
"password": "Admin12345"
}
}

signup = self.client.post(
"/api/users/",
data,
format="json"
)
activate = self.client.post(
"/api/users/activate/?uid={a}&token={b}".format(a=signup.data.get('id'),
b=signup.data.get('token')),
format="json"
)

self.assertEqual(activate.status_code,200)
self.assertEqual(activate.data.get('message'),
'Your account has been activated.')

1 change: 1 addition & 0 deletions authors/tests/data/test_signup.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,4 @@ def test_raises_error_if_duplicate_data_provided(self):
"username already exists" in
new_user.data["errors"]["username"][0]
)

7 changes: 6 additions & 1 deletion authors/tests/data/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ def setUp(self):

BaseTest.setUp(self)

self.signup_user()
signup = self.signup_user()
uid = signup.data.get('id')
token = signup.data.get('token')

self.activate_user(uid=uid, token=token)

def test_successful_fetch_if_authorization_provided(self):
"""
Expand Down Expand Up @@ -123,3 +127,4 @@ def test_raises_forbidden_if_incorrect_authorization_credentials(self):
self.update_user_details().status_code,
status.HTTP_403_FORBIDDEN
)

3 changes: 0 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ django-rest-swagger==2.1.2
djangorestframework==3.9.2
idna==2.8
django-mailer==1.2.6
djangorestframework==3.9.2
GDAL==2.4.1
isort==4.3.17
itypes==1.1.0
Jinja2==2.10.1
Expand All @@ -21,7 +19,6 @@ MarkupSafe==1.1.1
lockfile==0.12.2
mccabe==0.6.1
nose==1.3.7
numpy==1.16.2
pep8==1.7.1
psycopg2==2.8.2
psycopg2-binary==2.8.2
Expand Down

0 comments on commit ac3d5e6

Please sign in to comment.