Skip to content

Commit

Permalink
feat(email verification): Users can verify their accounts
Browse files Browse the repository at this point in the history
- Users can verify their accounts via email
- Unverified users cannot log in
- Emails can now be sent via the sendgrid smtp server
- Users can request new verification links
- Users must send callback url when registering accounts

[starts #164046247]
  • Loading branch information
MandelaK committed Mar 14, 2019
1 parent 1b578ab commit 969bece
Show file tree
Hide file tree
Showing 22 changed files with 746 additions and 167 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,4 @@ migrations/
.DS_Store
.vscode/
staticfiles/
sendgrid.env
121 changes: 58 additions & 63 deletions authors/apps/articles/tests/test_article_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@
from rest_framework import status
from rest_framework.test import APIClient

from authors.apps.authentication.models import User


class ArticleViewsTestCase(TestCase):
'''This class implements tests for the article model'''

def setUp(self):
self.user1 = {
"user":{
"email": "user1@mail.com",
"username": "user1",
"password": "user1user1"
}
}

self.user2 = {
"user":{
"email": "user2@mail.com",
"username": "user2",
"password": "user2user2"
}
}
# create a user and verify then so they can log in
self.user1 = User.objects.create_user(
username='user1', email='user1@mail.com', password='user1user1')
self.user1.is_verified = True
self.user1.save()

self.user2 = User.objects.create_user(
username='user2', email='user2@mail.com', password='user2user2')
self.user2.is_verified = True
self.user2.save()

self.user1_credentials = {
"user": {
Expand All @@ -48,21 +47,18 @@ def setUp(self):
}

client = APIClient()
#Register Users
client.post(reverse('authentication:register'), self.user1, format='json')
client.post(reverse('authentication:register'), self.user2, format='json')

#Login Users
user1_login_data = client.post(reverse('authentication:login'),
self.user1_credentials, format='json')
user2_login_data = client.post(reverse('authentication:login'),
self.user2_credentials, format='json')

#Get tokens for the 2 users
token_user1 = user1_login_data.data['token']
token_user2 = user2_login_data.data['token']

#Create login headers for the two users

# Login Users
user1_login_data = client.post(reverse('authentication:login'),
self.user1_credentials, format='json')
user2_login_data = client.post(reverse('authentication:login'),
self.user2_credentials, format='json')

# Get tokens for the 2 users
token_user1 = user1_login_data.data.get('token')
token_user2 = user2_login_data.data.get('token')

# Create login headers for the two users
self.header_user1 = {
'HTTP_AUTHORIZATION': f'Bearer {token_user1}'
}
Expand All @@ -73,7 +69,7 @@ def setUp(self):
def test_create_article_success(self):
client = APIClient()
response = client.post(reverse('articles:create_article'),
self.sample_input,**self.header_user1, format='json')
self.sample_input, **self.header_user1, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_create_article_invalid_data(self):
Expand All @@ -84,25 +80,25 @@ def test_create_article_invalid_data(self):
}
client = APIClient()
response = client.post(reverse('articles:create_article'),
sample_input,**self.header_user1, format='json')
self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)
sample_input, **self.header_user1, format='json')
self.assertEqual(response.status_code,
status.HTTP_400_BAD_REQUEST)

def test_get_all_articles_not_found(self):
client = APIClient()
response = client.get(reverse('articles:get_article'),
format='json')
self.assertEqual(response.status_code,
status.HTTP_404_NOT_FOUND)
format='json')
self.assertEqual(response.status_code,
status.HTTP_404_NOT_FOUND)

def test_get_all_articles_success(self):
#Create article with user1
# Create article with user1
client = APIClient()
respo = client.post(reverse('articles:create_article'),
self.sample_input,**self.header_user1, format='json')
self.sample_input, **self.header_user1, format='json')
self.assertEqual(respo.status_code, status.HTTP_201_CREATED)

#Publish article of user1
# Publish article of user1
my_url = '/api/articles/{}/edit'.format(respo.data['slug'])
respo1 = client.patch(
my_url,
Expand All @@ -111,19 +107,19 @@ def test_get_all_articles_success(self):
)
self.assertEqual(respo1.status_code, status.HTTP_200_OK)

#Get all articles
# Get all articles
response = client.get(reverse('articles:get_article'),
format='json')
self.assertEqual(response.status_code,
status.HTTP_200_OK)
format='json')
self.assertEqual(response.status_code,
status.HTTP_200_OK)

def test_get_an_article(self):
client = APIClient()
respo = client.post(reverse('articles:create_article'),
self.sample_input,**self.header_user1, format='json')
self.sample_input, **self.header_user1, format='json')
self.assertEqual(respo.status_code, status.HTTP_201_CREATED)

#Get an article that is not there
# Get an article that is not there
my_url = '/api/articles/{}'.format(respo.data['slug'])
respo1 = client.get(
my_url,
Expand All @@ -132,7 +128,7 @@ def test_get_an_article(self):
)
self.assertEqual(respo1.status_code, status.HTTP_404_NOT_FOUND)

#Publish article of user1
# Publish article of user1
my_url = '/api/articles/{}/edit'.format(respo.data['slug'])
respo2 = client.patch(
my_url,
Expand All @@ -141,7 +137,7 @@ def test_get_an_article(self):
)
self.assertEqual(respo2.status_code, status.HTTP_200_OK)

#Get an article that is there
# Get an article that is there
my_url = '/api/articles/{}'.format(respo.data['slug'])
respo3 = client.get(
my_url,
Expand All @@ -150,15 +146,14 @@ def test_get_an_article(self):
)
self.assertEqual(respo3.status_code, status.HTTP_200_OK)


def test_update_article_does_not_exist(self):
#Create article with user1
# Create article with user1
client = APIClient()
respo = client.post(reverse('articles:create_article'),
self.sample_input,**self.header_user1, format='json')
self.sample_input, **self.header_user1, format='json')
self.assertEqual(respo.status_code, status.HTTP_201_CREATED)

#Publish article of user1
# Publish article of user1
my_url = '/api/articles/{}/edit'.format(respo.data['slug'])
respo1 = client.patch(
my_url,
Expand All @@ -167,15 +162,15 @@ def test_update_article_does_not_exist(self):
)
self.assertEqual(respo1.status_code, status.HTTP_200_OK)

#Delete article of user1
# Delete article of user1
respo1 = client.delete(
my_url,
**self.header_user1,
format='json'
)
self.assertEqual(respo1.status_code, status.HTTP_200_OK)

#Try to update the article
# Try to update the article
respo1 = client.put(
my_url,
self.sample_input,
Expand All @@ -184,16 +179,14 @@ def test_update_article_does_not_exist(self):
)
self.assertEqual(respo1.status_code, status.HTTP_404_NOT_FOUND)



def test_update_article_successfully(self):
#Create article with user1
# Create article with user1
client = APIClient()
respo = client.post(reverse('articles:create_article'),
self.sample_input,**self.header_user1, format='json')
self.sample_input, **self.header_user1, format='json')
self.assertEqual(respo.status_code, status.HTTP_201_CREATED)

#Publish article of user1
# Publish article of user1
my_url = '/api/articles/{}/edit'.format(respo.data['slug'])
respo1 = client.patch(
my_url,
Expand All @@ -212,16 +205,18 @@ def test_update_article_successfully(self):
}
}

#Try to update the article with wrong user
# Try to update the article with wrong user
respo1 = client.put(
my_url,
update_input,
**self.header_user2,
format='json'
)
self.assertEqual(respo1.status_code, status.HTTP_401_UNAUTHORIZED)
self.assertEqual(respo1.data['detail'],
'You are not the owner of the article.')

#Try to update the article with right user
# Try to update the article with right user
respo1 = client.put(
my_url,
update_input,
Expand All @@ -238,13 +233,13 @@ def test_publish_article_with_no_draft(self):
"body": "You have to believe"
}
}
#Create article with user1
# Create article with user1
client = APIClient()
respo = client.post(reverse('articles:create_article'),
sample_input,**self.header_user1, format='json')
sample_input, **self.header_user1, format='json')
self.assertEqual(respo.status_code, status.HTTP_201_CREATED)

#Publish article of user1
# Publish article of user1
my_url = '/api/articles/{}/edit'.format(respo.data['slug'])
respo1 = client.patch(
my_url,
Expand Down
2 changes: 2 additions & 0 deletions authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def create_superuser(self, username, email, password):
user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.is_verified = True
user.save()

return user
Expand All @@ -70,6 +71,7 @@ class User(AbstractBaseUser, PermissionsMixin):
# but we can still analyze the data.
is_active = models.BooleanField(default=True)

# users must verify their accounts before they are able to log in.
is_verified = models.BooleanField(default=False)

# The `is_staff` flag is expected by Django to determine who can and cannot
Expand Down
62 changes: 60 additions & 2 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.contrib.auth import authenticate
from django.core.validators import RegexValidator
from django.core.validators import RegexValidator, URLValidator
from rest_framework import serializers
from rest_framework.validators import UniqueValidator

from .models import User
from authors.apps.profiles.serializers import GetProfileSerializer

Expand Down Expand Up @@ -29,11 +30,22 @@ class RegistrationSerializer(serializers.ModelSerializer):
}
)

callback_url = serializers.URLField(
write_only=True,
validators=[URLValidator(
message='Callback URL should be a valid URL',
code='invalid_callback_url'
)],
error_messages={
'invalid_url': 'Please check that the callback URL is a valid URL'
}
)

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 = ['email', 'username', 'password', 'callback_url']
extra_kwargs = {
'email': {
'error_messages': {
Expand Down Expand Up @@ -62,6 +74,7 @@ class Meta:

def create(self, validated_data):
# Use the `create_user` method we wrote earlier to create a new user.
validated_data.pop('callback_url')
return User.objects.create_user(**validated_data)


Expand Down Expand Up @@ -116,6 +129,11 @@ def validate(self, data):
'This user has been deactivated.'
)

if not user.is_verified:
raise serializers.ValidationError(
'Please verify your account to proceed.'
)

# Django provides a flag on our `User` model called `is_active`. The
# purpose of this flag to tell us whether the user has been banned
# or otherwise deactivated. This will almost never be the case, but
Expand Down Expand Up @@ -203,3 +221,43 @@ class SocialAuthenticationSerializer(serializers.Serializer):
access_token_secret = serializers.CharField(
max_length=500, allow_blank=True)
provider = serializers.CharField(max_length=500, required=True)


class CreateEmailVerificationSerializer(serializers.Serializer):
"""Create a new token for email verification"""
email = serializers.EmailField(required=True)
username = serializers.CharField(required=True)
callback_url = serializers.URLField(
write_only=True, required=True
)

class Meta:
fields = ['email', 'callback_url', 'username']

def create_payload(self, data):
email = data.get('email', None)
username = data.get('username', None)
callback_url = data.get('callback_url', None)

try:
user = User.objects.get(email=email)
except User.DoesNotExist:
raise serializers.ValidationError(
'No user with this email address is registered.'
)

if user.username != username or user.email != email:
raise serializers.ValidationError(
"Your username and email don't match."
)

if user.is_verified:
raise serializers.ValidationError(
'This user has already been verified'
)

return {
'email': email,
'username': username,
'callback_url': callback_url
}
Loading

0 comments on commit 969bece

Please sign in to comment.