Skip to content

Commit

Permalink
164047059-feature(email verification): Implements email verification
Browse files Browse the repository at this point in the history
feature
- ensures user receives an email once he/she signs up
- ensures user email is validated using a token
- adds .env.example field for the upodated envs

[Finishes #164047059]
  • Loading branch information
Kibetchirchir committed Mar 5, 2019
1 parent b5bd0ce commit 6438bd2
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 12 deletions.
9 changes: 6 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export HOST=localhost # The default is usually localhost
export DB_NAME=authors_haven # Create a database and use this
export DB_USER=postgres # Use your postgres user. The default is usually postgres
export DB_PASSWORD='' # Use your database password
export DB_NAME=olympians # Create a database and use this
export DB_USER=name # Use your postgres user. The default is usually postgres
export DB_PASSWORD='' # Use your database password
export SENDGRID_API_KEY='API_KEY here' #This is the sendgrid api key
export EMAIL_FROM="test@test.com" # This is the email that is sending the email
export URL="http://127.0.0.1:8000/" #The base url where the frontend app is hosted
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,3 @@ db.sqlite3

#ds_store
.ds_store

35 changes: 34 additions & 1 deletion authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import jwt
import uuid

from datetime import datetime, timedelta

Expand All @@ -13,7 +14,6 @@ class UserManager(BaseUserManager):
"""
Django requires that custom users define their own Manager class. By
inheriting from `BaseUserManager`, we get a lot of the same code used by
Django to create a `User` for free.
All we have to do is override the `create_user` function which we will use
to create `User` objects.
"""
Expand Down Expand Up @@ -139,3 +139,36 @@ def _generate_jwt_token(self):
}, settings.SECRET_KEY, algorithm='HS256')

return token.decode('utf-8')

def create_token(self):
"""
This
:return:
"""
token = str(uuid.uuid4())
verification = EmailVerification.objects.create(user=self, token=token)
verification.save()
return verification.token


class EmailVerification(models.Model):
"""
This class creates a Password Reset Token.
"""
user = models.ForeignKey(
User,
related_name='email_verifications',
on_delete=models.CASCADE,
verbose_name='User associated with this email token'
)
token = models.CharField(max_length=256)
created = models.DateTimeField(
auto_now=True,
verbose_name='When was this TOKEN created'
)
is_valid = models.BooleanField(default=True)

class Meta:
ordering = ('created',)

12 changes: 11 additions & 1 deletion authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from rest_framework import serializers

from .models import User
from .models import User, EmailVerification


class RegistrationSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -177,3 +177,13 @@ def update(self, instance, validated_data):
instance.save()

return instance


class EmailVerificationSerializer(serializers.ModelSerializer):
"""
This the email verification Serializer
"""
class Meta:
model = EmailVerification
fields = ('token',)
extra_kwargs = {"token": {"error_messages": {"required": "Please provide a token"}}}
35 changes: 35 additions & 0 deletions authors/apps/authentication/tests/test_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import json
from rest_framework.test import APITestCase, APIClient
from rest_framework.views import status
import uuid
import unittest

from ..utils import send_email, verify_message
from ..models import User


class TestEmailIntergration(unittest.TestCase):
"""
This class tests sending email with sendgrid and
"""
def test_success_email(self):
self.assertEqual(send_email("test@test.com", "test", "test"), 'email sent')

def test_failed_email(self):
"""
Testing providing a string instead of email
"""
self.assertEqual(send_email("testtestcom", "test", "test"), 'There was an error sending')


class TestVerificationMessage(unittest.TestCase):
"""
This class tests sending email with sendgrid and
"""
def test_name_message(self):
self.assertIn("Chirchir", verify_message("Chirchir", "token"))

def test_token_message(self):
token = str(uuid.uuid4())
self.assertIn(token, verify_message("Chirchir", token))

73 changes: 73 additions & 0 deletions authors/apps/authentication/tests/test_email_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from django.shortcuts import reverse
import json
from rest_framework.test import APITestCase, APIClient
from rest_framework.views import status

from ..models import EmailVerification, User


class TestEmailVerification(APITestCase):
"""
Class tests for email verification .
"""
client = APIClient()

def setUp(self):
""" Creates user and user dictionary for testing."""
self.user = {
"user": {
"email": "chirchir@olympians.com",
"username": "chirchir",
"password": "test1234"
}
}

def test_success_verification(self):
""" Tests the token is created and saved on the db."""
response = self.client.post('/api/users/', self.user, format='json')
self.assertEqual(EmailVerification.objects.count(), 1)

def test_token_verified(self):
"""
tests the token is verified and used
"""
self.client.post('/api/users/', self.user, format='json')
user1 = User.objects.get(username ='chirchir')
verification = EmailVerification.objects.filter(user=user1).first()
token = verification.token
token_verify = {"token": token}
response = self.client.post('/api/users/verify/', token_verify, format='json')
result = json.loads(response.content)

self.assertEqual(result["user"]["success"], "valid token")
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_token_already_used(self):
"""
tests for an already used token by sending multiple tests
"""
self.client.post('/api/users/', self.user, format='json')
user1 = User.objects.get(username='chirchir')
verification = EmailVerification.objects.filter(user=user1).first()
token = verification.token
token_verify = {"token": token}
self.client.post('/api/users/verify/', token_verify, format='json')
response = self.client.post('/api/users/verify/', token_verify, format='json')
result = json.loads(response.content)

self.assertEqual(result["user"]["error"], "Token already used")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_token_doesnt_exist(self):
"""
Tests for a non existing token
"""
self.client.post('/api/users/', self.user, format='json')
user1 = User.objects.get(username='chirchir')
verification = EmailVerification.objects.filter(user=user1).first()
token_verify = {"token": "test"}
response = self.client.post('/api/users/verify/', token_verify, format='json')
result = json.loads(response.content)

self.assertEqual(result["user"]["error"], "invalid token")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
3 changes: 2 additions & 1 deletion authors/apps/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.urls import path

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

app_name = 'authentication'
Expand All @@ -10,4 +10,5 @@
path('user/', UserRetrieveUpdateAPIView.as_view()),
path('users/', RegistrationAPIView.as_view()),
path('users/login/', LoginAPIView.as_view()),
path('users/verify/', UserEmailVerification.as_view()),
]
28 changes: 28 additions & 0 deletions authors/apps/authentication/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import sendgrid
import os
from sendgrid.helpers.mail import *


def send_email(to_email, subject, message):
sg = sendgrid.SendGridAPIClient(apikey=os.getenv("SENDGRID_API_KEY"))
from_email = Email(os.getenv("EMAIL_FROM"))
to_email = Email(to_email)
subject = subject
content = Content("text/plain", message)
try:
mail = Mail(from_email, subject, to_email, content)
response = sg.client.mail.send.post(request_body=mail.get())
# response code 202 ensures the message is sent
if response.status_code is not 202:
return "email not sent check your api key and email from"
return "email sent"
except Exception:
return "There was an error sending"


def verify_message(name, token):
message = "Thank you " + name + " for registering with us please verify your email\n" \
" by clicking on the following link\n" \
+ os.getenv("URL") + "/verify/" + token + "\n Welcome"

return message
35 changes: 30 additions & 5 deletions authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .utils import send_email, verify_message

from .models import EmailVerification
from .renderers import UserJSONRenderer
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer
LoginSerializer, RegistrationSerializer, UserSerializer, EmailVerificationSerializer
)


Expand All @@ -17,14 +19,18 @@ class RegistrationAPIView(APIView):
serializer_class = RegistrationSerializer

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

new_user_data = request.data.get('user', {})
# The create serializer, validate serializer, save serializer pattern
# below is common and you will see it a lot throughout this course and
# your own work later on. Get familiar with it.
serializer = self.serializer_class(data=user)
serializer = self.serializer_class(data=new_user_data)
serializer.is_valid(raise_exception=True)
serializer.save()
user = serializer.save()
token = user.create_token()
user_email = user.email
username = user.username
sign_up_message = verify_message(username, token)
send_email(user_email, "verify", sign_up_message)

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

Expand Down Expand Up @@ -72,3 +78,22 @@ def update(self, request, *args, **kwargs):
serializer.save()

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


class UserEmailVerification(APIView):
renderer_classes = (UserJSONRenderer,)
serializer_class = EmailVerificationSerializer

def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
token = serializer.data['token']
try:
verification = EmailVerification.objects.get(token=token)
if not verification.is_valid:
return Response({"error": "Token already used"}, status=status.HTTP_400_BAD_REQUEST)
verification.is_valid = False
verification.save()
return Response({"success":"valid token"}, status=status.HTTP_200_OK)
except EmailVerification.DoesNotExist:
return Response({"error": "invalid token"}, status=status.HTTP_403_FORBIDDEN)
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ mccabe==0.6.1
psycopg2-binary==2.7.7
PyJWT==1.7.1
pylint==2.2.2
python-http-client==3.1.0
pytz==2018.9
sendgrid==5.6.0
six==1.12.0
whitenoise==4.1.2
wrapt==1.11.1

0 comments on commit 6438bd2

Please sign in to comment.