Skip to content

Commit

Permalink
Merge b4d315e into 857c9eb
Browse files Browse the repository at this point in the history
  • Loading branch information
MaggieChege committed Jan 18, 2019
2 parents 857c9eb + b4d315e commit 98ed233
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 19 deletions.
11 changes: 11 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source env/bin/activate
export DB_NAME="Your DB"
export DB_USER="DB User"
export DB_PASS="DB Password"
export DB_HOST="localhost"
export DB_PORT="DB port"
export EMAIL_HOST_USER="host username"
export EMAIL_HOST_PASSWORD="email password"
export PASSWORD_RESET_URL="127.0.0.1:8000/api"
export DEFAULT_FROM_EMAIL="senders email"
export SECRET_KEY="enter your secret key"
48 changes: 36 additions & 12 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import re

from django.contrib.auth import authenticate
from rest_framework import serializers

from .models import User


class RegistrationSerializer(serializers.ModelSerializer):
"""Serializers registration requests and creates a new user."""

# Ensure passwords are at least 8 characters long,contain alphanumerics
# special characters and
# no longer than 128
# characters, and can not be read by the client.
password = serializers.RegexField(
def password_validate():
return serializers.RegexField(
regex='^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[@#$%^&+=*!])',
max_length=128,
min_length=8,
Expand All @@ -23,8 +15,18 @@ class RegistrationSerializer(serializers.ModelSerializer):
'min_length': 'Ensure Password field has at least 8 characters',
'invalid': 'Password should contain a lowercase, uppercase numeric'
' and special character'
}
)
})


class RegistrationSerializer(serializers.ModelSerializer):
"""Serializers registration requests and creates a new user."""

# Ensure passwords are at least 8 characters long,contain alphanumerics
# special characters and
# no longer than 128
# characters, and can not be read by the client.

password = password_validate()

# Ensure username doesnt have special characters or numbers only
# Ensure username is greater than six
Expand Down Expand Up @@ -121,6 +123,28 @@ def validate(self, data):
}


class ResetSerializerEmail(serializers.ModelSerializer):
"""Get users email"""
email = serializers.EmailField()

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

class Meta:
model = User
fields = ('email',)


class ResetSerializerPassword(serializers.ModelSerializer):
"""Validates Password"""
password = password_validate()
confirm_password = serializers.CharField()

class Meta:
model = User
fields = ('password', 'confirm_password')


class UserSerializer(serializers.ModelSerializer):
"""Handles serialization and deserialization of User objects."""
# Passwords must be at least 8 characters, but no more than 128
Expand Down
36 changes: 36 additions & 0 deletions authors/apps/authentication/templates/email_template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Authors Haven account verification</title>
<meta name="description" content="Our first page">
<meta name="keywords" content="html tutorial template">
</head>


<body>
<div id="div-page" style="border: 2px solid #86C232;border-radius: 25px;width: 30em;padding: 25px;margin: 5em;text-align: justify; border-width: thick">

<h1 style="line-height: 35px;text-align: center; color: #86C232;">
Authors Haven
</h1>
<h1 style="line-height: 35px;text-align: center;">
Hello <span id="username" style="color: #23bebe;">{{username}}</span>
</h1>

<div id="div-content" style="line-height: 26px;">
<p style="text-align: center;">Your requested for password reset!</p>
<p style="text-align: center;">Please reset your password by clicking the button below</p>
<div style="margin-top: 7%;">
<a id="confirm-link" href="{{link}}" style="background-color:
#23bebe; color:#fff;padding: 0.4em;font-size: 2em;text-decoration: none;margin-left: 19%;">Reset Password</a>
</div>
<br>
<p style="text-align: center;">Kindly note that the link expires in exactly one day as from {{time}}</p>

</div>
</div>


</body>

</html>
76 changes: 76 additions & 0 deletions authors/apps/authentication/tests/test_passwordreset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import json
import jwt
from datetime import datetime, timedelta
from django.conf import settings
from django.urls import reverse
from rest_framework.views import status
from rest_framework.test import APITestCase, APIClient


class ResetPasswordTestCase(APITestCase):
def setUp(self):
self.client = APIClient()
self.password_reset_url = reverse('authentication:passwordreset')
self.signup_url = reverse('authentication:auth-register')
self.signup_data = {
"user": {
"username": "MaryGigz",
"email": "chegemaggie1@gmail.com",
"password": "g@_Gigz-2416"
}}
self.email = {
"email": "chegemaggie1@gmail.com"
}
self.email_not_registered = {
"email": "kenna@gmail.com"
}
self.test_update_password_data = {
"password": "Jake@1234",
"confirm_password": "Jake@1234"

}

def register(self):
register = self.client.post(self.signup_url, self.signup_data,
format='json')
self.assertEqual(register.status_code, status.HTTP_201_CREATED)
return register

@staticmethod
def create_url(email):
"""create a url with the token, this is done after user receives an email"""
token = jwt.encode({"email": email,
"iat": datetime.now(),
"exp": datetime.utcnow() + timedelta(minutes=5)},
settings.SECRET_KEY,
algorithm='HS256').decode()
password_reset_url = reverse("authentication:passwordresetdone",
kwargs={"token": token})
return password_reset_url

def test_reset_password(self):
"""Test user can reset the password"""
self.register()
response = self.client.put(self.create_url("chegemaggie1@gmail.com"),
self.test_update_password_data,
format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_email_not_registered(self):
"""Test if user that is not registered can get the email"""
response = self.client.post(self.password_reset_url,
self.email_not_registered,
format="json")
self.assertEqual(response. status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(json.loads(response.content), {'errors': {'email':
['User with this email does not exist.']}})

def test_send_email(self):
"""First register a new user and check if user is registered"""
self.register()
response = self.client.post(self.password_reset_url,
self.email,
format="json")
self.assertEqual(response. status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content), {'message':
'Successfully sent.Check your email'})
7 changes: 6 additions & 1 deletion authors/apps/authentication/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@


from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView,
ResetPasswordAPIView, UpdatePasswordAPIView
)

app_name = "authentication"
urlpatterns = [
path('user/', UserRetrieveUpdateAPIView.as_view()),
path('users/', RegistrationAPIView.as_view(), name="auth-register"),
path('users/login/', LoginAPIView.as_view(), name="auth-login"),
path('users/passwordreset/', ResetPasswordAPIView.as_view(), name="passwordreset"),
path('users/passwordresetdone/<token>', UpdatePasswordAPIView.as_view(), name="passwordresetdone"),


]
83 changes: 81 additions & 2 deletions authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import os
import jwt
from datetime import datetime, timedelta
from django.conf import settings
from django.template.loader import render_to_string
from django.core.mail import send_mail
from rest_framework import status, generics
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BrowsableAPIRenderer
from .renderers import UserJSONRenderer
from . import serializers
from . import models
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer
LoginSerializer, RegistrationSerializer, UserSerializer,
ResetSerializerEmail, ResetSerializerPassword
)
from authors.apps.core.permissions import IsAuthorOrReadOnly


class RegistrationAPIView(generics.CreateAPIView):
Expand Down Expand Up @@ -72,3 +80,74 @@ def update(self, request, *args, **kwargs):
serializer.save()

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


class ResetPasswordAPIView(generics.CreateAPIView):
"""Sends password reset link to email"""
serializer_class = ResetSerializerEmail

def post(self, request):

email = request.data['email']
if email == "":
return Response({"errors": {
"email": ["An email is required"]}})
user = models.User.objects.filter(email=email)
if user:
token = jwt.encode({"email": email, "iat": datetime.now(),
"exp": datetime.utcnow() + timedelta(minutes=5)},
settings.SECRET_KEY, algorithm='HS256').decode()
to_email = [email]
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL")
host_url = os.getenv("PASSWORD_RESET_URL")
link = 'http://' + str(host_url) + '/users/passwordresetdone/' + token
message = render_to_string(
'email_template.html', {
'user': to_email,
'domain': link,
'token': token,
'username': to_email,
'link': link
})
send_mail('You requested password reset',
'Reset your password', 'DEFAULT_FROM_EMAIL',
[to_email, ], html_message=message, fail_silently=False)
message = {
"message": "Successfully sent.Check your email",
}
return Response(message, status=status.HTTP_200_OK)

else:
message = {"errors": {
"email": [
"User with this email does not exist."
]}
}
return Response(message, status=status.HTTP_400_BAD_REQUEST)


class UpdatePasswordAPIView(generics.UpdateAPIView):
"""Allows you to reset you password"""
serializer_class = ResetSerializerPassword

def put(self, request, token, **kwargs):
try:
password = request.data.get('password')
confirm_password = request.data.get('confirm_password')
if password != confirm_password:
return Response({"Passwords do not match"},
status=status.HTTP_200_OK)
serializer = self.serializer_class(data={"password": password,
"confirm_password": confirm_password})
serializer.is_valid(raise_exception=True)
decode_token = jwt.decode(token, settings.SECRET_KEY,
algorithms='HS256')
email = decode_token.get('email')
user = models.User.objects.get(email=email)
user.set_password(password)
user.save()
return Response({"message": "Password Successfully Updated"},
status=status.HTTP_200_OK)
except jwt.ExpiredSignatureError:
return Response({"The link expired"},
status=status.HTTP_400_BAD_REQUEST)
15 changes: 11 additions & 4 deletions authors/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"""

import os
import django_heroku
import django_heroku

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
Expand All @@ -20,7 +20,6 @@
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '7pgozr2jn7zs_o%i8id6=rddie!*0f0qy3$oy$(8231i^4*@u3'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
Expand All @@ -46,7 +45,7 @@
'authors.apps.profiles',

# Enables API to be documented using Swagger
'rest_framework_swagger',
'rest_framework_swagger',
]

MIDDLEWARE = [
Expand Down Expand Up @@ -170,4 +169,12 @@
},
},
}



SECRET_KEY = os.getenv('SECRET_KEY')
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_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = True

0 comments on commit 98ed233

Please sign in to comment.