Skip to content

Commit

Permalink
Merge 296c776 into 4557108
Browse files Browse the repository at this point in the history
  • Loading branch information
Nta1e committed Dec 11, 2018
2 parents 4557108 + 296c776 commit ca2e357
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 23 deletions.
Binary file added .DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,5 @@ db.sqlite3

# vscode directory
.vscode/
.DS_Store
*.swp
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[![Build Status](https://travis-ci.org/andela/ah-backend-athena.svg?branch=ch-travis-readme-badge-162163256)](https://travis-ci.org/andela/ah-backend-athena)
[![Coverage Status](https://coveralls.io/repos/github/andela/ah-backend-athena/badge.svg?branch=ch-coveralls-integration-162163259)](https://coveralls.io/github/andela/ah-backend-athena?branch=ch-coveralls-integration-162163259)
[![Coverage Status](https://coveralls.io/repos/github/andela/ah-backend-athena/badge.svg?branch=develop)](https://coveralls.io/github/andela/ah-backend-athena?branch=develop)

Authors Haven - A Social platform for the creative at heart.
=======
Expand Down
18 changes: 18 additions & 0 deletions authors/apps/authentication/migrations/0002_user_is_verified.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.1.3 on 2018-12-05 16:23

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('authentication', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='user',
name='is_verified',
field=models.BooleanField(default=False),
),
]
2 changes: 2 additions & 0 deletions authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class User(AbstractBaseUser, PermissionsMixin):
# but we can still analyze the data.
is_active = models.BooleanField(default=True)

is_verified = models.BooleanField(default=False)

# The `is_staff` flag is expected by Django to determine who can and cannot
# log into the Django admin site. For most users, this flag will always be
# falsed.
Expand Down
4 changes: 4 additions & 0 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ def validate(self, data):
raise serializers.ValidationError(
'A user with this email and password was not found.'
)
if user.is_verified == False:
raise serializers.ValidationError(
'Your email is not verified, Please check your email for a verification link'
)

# 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
Expand Down
49 changes: 44 additions & 5 deletions authors/apps/authentication/test/test_authentication.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import json
from django.urls import reverse
from rest_framework.views import status
from rest_framework.test import APITestCase, APIClient
from rest_framework.test import APITestCase, APIClient, APIRequestFactory
from ..views import VerifyAccount, RegistrationAPIView
from ..models import UserManager, User


Expand All @@ -20,6 +21,17 @@ def generate_user(self, username='', email='', password=''):
}
return user

def verify_account(self, token, uidb64):
request = APIRequestFactory().get(
reverse(
"activate_account",
kwargs={
"token": token,
"uidb64": uidb64}))
verify_account = VerifyAccount.as_view()
response = verify_account(request, token=token, uidb64=uidb64)
return response

def create_user(self, username='', email='', password=''):
user = self.generate_user(username, email, password)
self.client.post('/api/users/', user, format='json')
Expand All @@ -31,10 +43,23 @@ def test_user_registration(self):
response = self.client.post('/api/users/', user, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
json.loads(response.content),
{"user": {"email": "athena@gmail.com", "username": "athena"}}
)

json.loads(
response.content), {
"user": {
"message": "A verification email has been sent to athena@gmail.com"}})

def test_cannot_login_without_verification(self):
self.create_user('athena', 'athena@gmail.com', 'password@user')
login_details = self.generate_user(
'', 'athena@gmail.com', 'password@user')
response = self.client.post(
'/api/users/login/', login_details, format='json')
self.assertEqual(
json.loads(
response.content), {
"errors": {
"error": ["Your email is not verified, Please check your email for a verification link"]}})

def test_user_registration_empty_details(self):
user = self.generate_user('', '', '')
response = self.client.post('/api/users/', user, format='json')
Expand All @@ -49,6 +74,13 @@ def test_user_login(self):
self.create_user('athena', 'athena@gmail.com', 'password@user')
login_details = self.generate_user(
'', 'athena@gmail.com', 'password@user')
request = APIRequestFactory().post(
reverse("registration")
)
user = User.objects.get()
token, uidb64 = RegistrationAPIView.generate_activation_link(
user, request, send=False)
self.verify_account(token, uidb64)
response = self.client.post(
'/api/users/login/', login_details, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
Expand Down Expand Up @@ -81,6 +113,13 @@ def test_user_with_valid_token_access_protected_endpoints(self):
self.create_user('soko', 'athena@gmail.com', 'Password@user1')
login_details = self.generate_user(
'', 'athena@gmail.com', 'Password@user1')
request = APIRequestFactory().post(
reverse("registration")
)
user = User.objects.get()
token, uidb64 = RegistrationAPIView.generate_activation_link(
user, request, send=False)
self.verify_account(token, uidb64)
response = self.client.post(
'/api/users/login/', login_details, format='json')
token = response.data['token']
Expand Down
97 changes: 97 additions & 0 deletions authors/apps/authentication/test/test_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import json
from unittest.mock import patch

from django.urls import reverse
from django.core import mail
from rest_framework.test import APITestCase, APIRequestFactory
from rest_framework import status
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.encoding import force_bytes, force_text

from ..models import User
from ..views import RegistrationAPIView, VerifyAccount


class VerificationTestCase(APITestCase):
def setUp(self):
self.data = {
"user": {
"username": "Ntale",
"email": "shadik.ntale@andela.com",
"password": "AthenaD0a"
}
}
self.url = reverse('registration')
self.client.post(self.url, self.data, format='json')
self.request = APIRequestFactory().post(
reverse("registration"))

def verify_account(self, token, uidb64):
request = APIRequestFactory().get(
reverse(
"activate_account",
kwargs={
"token": token,
"uidb64": uidb64}))
verify_account = VerifyAccount.as_view()
response = verify_account(request, token=token, uidb64=uidb64)
return response

def test_sends_email(self):
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(
mail.outbox[0].subject,
"Author's Heaven account email verification")

def test_account_verified(self):
user = User.objects.get()
req = self.request
token, uidb64 = RegistrationAPIView.generate_activation_link(
user, req, send=False)
response = self.verify_account(token, uidb64)
self.assertEqual(response.status_code, status.HTTP_200_OK)

user = User.objects.get()
self.assertTrue(user.is_verified)

def test_generate_activation_link_function(self):
user = User.objects.get()
request = self.request
token, uid = RegistrationAPIView.generate_activation_link(user, request, send=False)
self.assertFalse(token==None)
self.assertFalse(uid==None)
self.assertFalse(len(mail.outbox)==2)

def test_send_activation_link(self):
with patch('authors.apps.authentication.views.send_mail') as mock_mail:
user = User.objects.get()
request = self.request

RegistrationAPIView.generate_activation_link(
user, request, send=False)
self.assertFalse(mock_mail.called)

token, uid = RegistrationAPIView.generate_activation_link(
user, request, send=True)
self.assertTrue(mock_mail.called)
mock_mail.assert_called_with(
subject="Author's Heaven account email verification",
from_email="athenad0a@gmail.com",
recipient_list=['shadik.ntale@andela.com'],
message="Please follow the following link to activate your account \n https://testserver/api/activate/{}/{}/".format(token, uid),
fail_silently=False)

def test_invalid_verification_link(self):
request = self.request

user = User.objects.get()
token, uid = RegistrationAPIView.generate_activation_link(user, request, send=False)

# create the uid from a different username
uid = urlsafe_base64_encode(force_bytes("invalid_username")).decode("utf-8")

response = self.verify_account(token, uid)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
user = User.objects.get()
# Ensure the user is not verified
self.assertFalse(user.is_verified)
7 changes: 3 additions & 4 deletions authors/apps/authentication/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@

from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView,
PasswordResetView, PasswordResetConfirmView,
)

PasswordResetView, PasswordResetConfirmView, VerifyAccount)
urlpatterns = [
path('user/', UserRetrieveUpdateAPIView.as_view()),
path('users/', RegistrationAPIView.as_view()),
path('users/', RegistrationAPIView.as_view(), name="registration"),
path('users/login/', LoginAPIView.as_view()),
path('password_reset/', PasswordResetView.as_view()),
path('password_reset_confirm/<slug>', PasswordResetConfirmView.as_view()),
path('activate/<token>/<uidb64>/', VerifyAccount.as_view(), name="activate_account"),
]
87 changes: 76 additions & 11 deletions authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer
)
from .renderers import UserJSONRenderer
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.encoding import force_bytes, force_text
from django.contrib.sites.shortcuts import get_current_site

from rest_framework import status
from rest_framework import generics
from rest_framework.permissions import AllowAny, IsAuthenticated
Expand All @@ -11,32 +13,37 @@
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer
)
from authors.apps.profiles.models import Profile
from ..profiles.models import Profile
from .renderers import UserJSONRenderer

from .models import User
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import (
urlsafe_base64_decode, urlsafe_base64_encode, force_bytes,
)
from django.utils.http import force_bytes
from django.contrib.auth.hashers import make_password
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer,
PasswordResetSerializer, PasswordResetConfirmSerializer
)
from django.contrib.sites.shortcuts import get_current_site
from authors.apps.profiles.models import Profile

from django.core.mail import send_mail
from django.conf import settings
from datetime import timedelta
from django.core.signing import TimestampSigner

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


class RegistrationAPIView(GenericAPIView):
# Allow any user (authenticated or not) to hit this endpoint.
permission_classes = (AllowAny,)
renderer_classes = (UserJSONRenderer,)
serializer_class = RegistrationSerializer

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

# The create serializer, validate serializer, save serializer pattern
Expand All @@ -45,8 +52,41 @@ def post(self, request):
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
serializer.save()
address = serializer.data['email']
user = User.objects.filter(email=user['email']).first()

return Response(serializer.data, status=status.HTTP_201_CREATED)
RegistrationAPIView.generate_activation_link(user, request)
return Response({"message": "A verification email has been sent to {}".format(
address)}, status=status.HTTP_201_CREATED)

@staticmethod
def generate_activation_link(user, request=None, send=True):
"""
This method creates a custom actvation link that will be used when verifying
a user's email containing a token and a unique id. The generated token bares the user's identity
and the unique id is just an encoded byte string of the user's username
"""
token = default_token_generator.make_token(user)
uidb64 = urlsafe_base64_encode(
force_bytes(user.username)).decode()
domain = 'https://{}'.format(get_current_site(request))
subject = 'Author\'s Heaven account email verification'
route = "api/activate"
url = "{}/{}/{}/{}/".format(domain, route, token, uidb64)
message = 'Please follow the following link to activate your account \n {}'.format(
url)
from_email = settings.EMAIL_HOST_USER
to_list = [user.email]
if send:
send_mail(
subject=subject,
from_email=from_email,
recipient_list=to_list,
message=message,
fail_silently=False)

return token, uidb64


class LoginAPIView(GenericAPIView):
Expand Down Expand Up @@ -176,3 +216,28 @@ def update(self, request, **kwargs):
]
}
}, status=status.HTTP_400_BAD_REQUEST)

class VerifyAccount(GenericAPIView):

def get(self, request, token, uidb64):
"""
Here, I am verifying both the token and encoded byte string embeded in the activation link
by checking the token against the bearer username and also the encoded byte string
"""
username = force_text(urlsafe_base64_decode(uidb64))

user = User.objects.filter(username=username).first()
validate_token = default_token_generator.check_token(user, token)

data = {"message": "Your account has been verified, You can login now!"}
stat = status.HTTP_200_OK

if not validate_token:
data['message'] = "Your activation link is Invalid or has expired."
stat = status.HTTP_400_BAD_REQUEST
else:
user.is_verified = True
user.save()

return Response(data, status=stat)
Loading

0 comments on commit ca2e357

Please sign in to comment.