Skip to content

Commit

Permalink
Merge PR #112 from WayneLambert/develop
Browse files Browse the repository at this point in the history
Add Two-Factor Authentication by Email
  • Loading branch information
WayneLambert authored Aug 18, 2021
2 parents c840353 + 59e338a commit 0c4c86e
Show file tree
Hide file tree
Showing 38 changed files with 761 additions and 203 deletions.
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,4 @@ celerybeat-schedule
dmypy.json

# Pyre type checker
.pyre/
.pyre/
3 changes: 2 additions & 1 deletion aa_project/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,5 @@
AWS_BASE_BUCKET_ADDRESS = os.environ['AWS_BASE_BUCKET_ADDRESS']

# Email Token Settings
EMAIL_TOKEN_EXPIRATION_IN_SECS = 300
EMAIL_CHALLENGE_EXPIRATION_IN_SECS = 60 * 5
EMAIL_TOKEN_EXPIRATION_IN_SECS = 60 * 60 * 24 * 28
3 changes: 2 additions & 1 deletion aa_project/settings/prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
'www.waynelambert.dev',
]

# Changes suggested from $ python3 manage.py check --deploy
# Changes suggested from $ python manage.py check --deploy
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_SSL_REDIRECT = True
Expand All @@ -36,4 +36,5 @@
RECAPTCHA_PRIVATE_KEY = os.environ['RECAPTCHA_PRIVATE_KEY']

# Django Two-Factor Auth Settings
TWO_FACTOR_LOGIN_TIMEOUT = 300
TWO_FACTOR_REMEMBER_COOKIE_AGE = 60 * 60 * 24 * 28
4 changes: 4 additions & 0 deletions aa_project/settings/pytest/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ def safe_summary(self, encoded):
PASSWORD_HASHERS = (
'aa_project.settings.pytest.pytest.SimplePasswordHasher',
)

# Email Token Settings
EMAIL_CHALLENGE_EXPIRATION_IN_SECS = 60 * 5
EMAIL_TOKEN_EXPIRATION_IN_SECS = 60 * 60 * 24 * 28
22 changes: 11 additions & 11 deletions apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,18 @@ class PostSerializer(serializers.ModelSerializer):
reading_time = serializers.IntegerField()
post_absolute_url = serializers.URLField(source='get_absolute_url')

author_username = serializers.CharField(source='author.username')
author_username = serializers.CharField(source='author.get_username')
author_first_name = serializers.CharField(source='author.first_name')
author_last_name = serializers.CharField(source='author.last_name')
author_full_name = serializers.CharField(source='author.user.full_name')
author_initials = serializers.CharField(source='author.user.initials')
author_display_name = serializers.CharField(source='author.user.display_name')
author_join_year = serializers.IntegerField(source='author.user.join_year')
author_view = serializers.IntegerField(source='author.user.author_view')
author_created_date = serializers.DateTimeField(source='author.user.created_date')
author_updated_date = serializers.DateTimeField(source='author.user.updated_date')
author_absolute_url = serializers.URLField(source='author.user.get_absolute_url')
author_profile_picture = serializers.ImageField(source='author.user.profile_picture')
author_full_name = serializers.CharField(source='author.get_full_name')
author_initials = serializers.CharField(source='author.profile.initials')
author_display_name = serializers.CharField(source='author.profile.display_name')
author_join_year = serializers.IntegerField(source='author.profile.join_year')
author_view = serializers.IntegerField(source='author.profile.author_view')
author_created_date = serializers.DateTimeField(source='author.profile.created_date')
author_updated_date = serializers.DateTimeField(source='author.profile.updated_date')
author_absolute_url = serializers.URLField(source='author.profile.get_absolute_url')
author_profile_picture = serializers.ImageField(source='author.profile.profile_picture')

categories = CategorySerializer(many=True, read_only=True)

Expand Down Expand Up @@ -105,4 +105,4 @@ class Meta:
def get_status(self, obj):
return obj.get_status_display() # pragma: no cover

ordering = ['-updated_date', '-publish_date']
ordering = ('-updated_date', '-publish_date', )
2 changes: 1 addition & 1 deletion apps/blog/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def item_description(self, item):
return truncatewords(item.content, 30)

def item_author_name(self, item):
return item.author.user.full_name
return item.author.get_full_name()

def item_pubdate(self, item):
return item.publish_date
Expand Down
2 changes: 1 addition & 1 deletion apps/blog/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
class PublishedManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset().filter(status=1)
return qs.prefetch_related('categories').select_related('author__user')
return qs.prefetch_related('categories').select_related('author__profile')
10 changes: 5 additions & 5 deletions apps/blog/templates/blog/components/meta_data.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
<small class="text-muted">
<strong>Categories:</strong>
{% for category in post.categories.all %}
<a class="theme-link" href="{{ category.get_absolute_url }}">
{{ category.name }}
</a>
<a class="theme-link" href="{{ category.get_absolute_url }}">
{{ category.name }}
</a>
{% if not forloop.last %} | {% endif %} {% endfor %}
</small>
</li>
Expand All @@ -44,8 +44,8 @@
<span class="time">{{ post.word_count|intcomma }} words</span> |
<span class="time">{{ post.reading_time }} min read</span>
{% if user.is_authenticated and request.user == author %} |
<a class="theme-link" href="{% url 'blog:post_update' post.slug %}">Update</a> |
<a class="theme-link" href="{% url 'blog:post_delete' post.slug %}">Delete</a>
<a class="theme-link" href="{% url 'blog:post_update' post.slug %}">Update</a> |
<a class="theme-link" href="{% url 'blog:post_delete' post.slug %}">Delete</a>
{% endif %}
</small>
</li>
Expand Down
8 changes: 4 additions & 4 deletions apps/blog/templates/blog/components/post_card.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ <h6 class="card-title">
<td>
<div class="mb-1">
<a class="theme-link font-weight-bolder" href="{% url 'blog:author_posts' post.author.username %}">
{{ post.author.user.display_name }}
{{ post.author.profile.display_name }}
</a>
</div>
<small class="text-muted">
<ul class="no-bullets">
<li><strong>Published:</strong> {{ post.publish_date|date:"D d-M-y" }}</li>
<li><strong>Updated:</strong> {{ post.updated_date|date:"D d-M-y" }}</li>
<li>
<strong>Categories:</strong>
<strong>Categories:</strong>
{% for category in post.categories.all|slice:":2" %}
<a class="theme-link" href="{{ category.get_absolute_url }}">{{ category.name }}</a>
{% if not forloop.last %} | {% endif %}
{% endfor %}
</li>
<li>
<span class="time">{{ post.word_count|intcomma }} words</span> |
<span class="time">{{ post.word_count|intcomma }} words</span> |
<span class="time">{{ post.reading_time }} min read</span>
</li>
</ul>
Expand All @@ -45,4 +45,4 @@ <h6 class="card-title">
</div>
</div>
</div>
</div>
</div>
4 changes: 2 additions & 2 deletions apps/blog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def get_queryset(self):
def get_context_data(self, **kwargs):
""" Get's the author's name/username for presenting in the template """
context = super().get_context_data(**kwargs)
context['display_name'] = Post.published.first().author.user.display_name
context['display_name'] = Post.published.first().author.profile.display_name
return context


Expand Down Expand Up @@ -159,7 +159,7 @@ def get_context_data(self, **kwargs):
""" Facilitates detail page's pagination buttons """
context = super().get_context_data(**kwargs)
context['author'] = Post.published.first().author
context['profile'] = context['author'].user
context['profile'] = context['author'].profile
posts = Post.published.all()
posts_count = len(posts)

Expand Down
32 changes: 30 additions & 2 deletions apps/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@

from django.contrib.auth.models import AnonymousUser
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.utils import timezone

import pytest

from django_otp.util import random_hex
from mixer.backend.django import mixer
from PIL import Image

from apps.blog.models import Category, Post
from apps.blog.tests import helpers
from apps.users.models import EmailToken
from apps.users.utils import get_challenge_expiration_timestamp


@pytest.fixture(scope='function')
Expand Down Expand Up @@ -43,10 +47,34 @@ def auth_user(client, django_user_model, test_password):
email='[email protected]',
password=test_password,
)
client.login(username=user.username, password=test_password)
client.login(username=user.get_username(), password=test_password)
return user


@pytest.fixture(scope='function')
def device_auth_user(client, auth_user, test_password):
""" An two-factor authenticated user object using device token """
auth_user.totpdevice_set.create(name='default', key=random_hex(), confirmed=True)
client.login(username=auth_user.get_username(), password=test_password)
return auth_user


@pytest.fixture(scope='function')
def email_auth_user(client, auth_user, test_password):
""" An two-factor authenticated user object using email token """
email_token = EmailToken.objects.create(
challenge_email_address=auth_user.email,
challenge_token='123456',
challenge_generation_timestamp=timezone.now(),
challenge_expiration_timestamp=get_challenge_expiration_timestamp(),
challenge_completed=True, # Emulates challenge passed
user_id=auth_user.pk
)
email_token.save()
client.login(username=auth_user.get_username(), password=test_password)
return auth_user


@pytest.fixture(scope='function')
def unauth_user():
""" An unauthenticated user (i.e. an anonymous user) """
Expand Down Expand Up @@ -95,7 +123,7 @@ def li_sec_user(django_user_model, client, test_password, **kwargs):
kwargs['last_name'] = 'Morse'
kwargs['email'] = '[email protected]'
user = django_user_model.objects.create_user(pk=3, **kwargs)
client.login(username=user.username, password=test_password)
client.login(username=user.get_username(), password=test_password)
return user


Expand Down
2 changes: 1 addition & 1 deletion apps/contacts/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ def test_full_name(self):

def test_contact_str(self):
contact = mixer.blend(Contact, first_name='Wayne', last_name='Lambert')
assert str(contact) == contact.full_name, 'Str should be set to full_name property'
assert contact.__str__() == contact.full_name, 'Str should be set to full_name property'
90 changes: 68 additions & 22 deletions apps/pages/templates/pages/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
<header class="header text-center">
<div class="force-overflow">
<nav class="navbar navbar-expand-lg navbar-dark" >
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navigation" aria-controls="navigation" aria-expanded="false" aria-label="Toggle navigation">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navigation"
aria-controls="navigation" aria-expanded="false" aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div id="navigation" class="collapse navbar-collapse flex-column">
<div class="profile-section pt-3 pt-lg-0">
<div class="profile-avatar rounded-circle">
<div class="inner">
<a href="{% url 'pages:home' %}"><img class="profile-image rounded-circle"
src="{% static 'assets/images/gravatar-500.jpg' %}" alt="Wayne Lambert">
<a href="{% url 'pages:home' %}"><img class="profile-image rounded-circle"
src="{% static 'assets/images/gravatar-500.jpg' %}" alt="Wayne Lambert"
>
</a>
</div>
</div>
Expand All @@ -28,20 +31,38 @@
<a class="nav-link" href="{% url 'pages:home' %}"><i class="fa fa-home mr-2"></i>Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'pages:portfolio' %}"><i class="fa fa-briefcase mr-2"></i>Portfolio Projects</a>
<a class="nav-link" href="{% url 'pages:portfolio' %}">
<i class="fa fa-briefcase mr-2"></i>
Portfolio Projects
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'cv:cv' %}"><i class="fa fa-align-left mr-2"></i>CV</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-blog fa-fw mr-1"></i>Blog
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
>
<i class="fas fa-blog fa-fw mr-1"></i>
Blog
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{% url 'blog:home' %}"><i class="fas fa-home fa-fw mr-2"></i>Home</a>
<a class="dropdown-item" href="{% url 'blog:index' %}"><i class="fa fa-list mr-2"></i>Index</a>
<a class="dropdown-item" href="{% url 'blog:contents' %}"><i class="fa fa-info-circle mr-2"></i>Contents</a>
<a class="dropdown-item" href="{% url 'blog:search' %}"><i class="fa fa-search mr-2"></i>Search</a>
<a class="dropdown-item" href="{% url 'blog:home' %}">
<i class="fas fa-home fa-fw mr-2"></i>
Home
</a>
<a class="dropdown-item" href="{% url 'blog:index' %}">
<i class="fa fa-list mr-2"></i>
Index
</a>
<a class="dropdown-item" href="{% url 'blog:contents' %}">
<i class="fa fa-info-circle mr-2"></i>
Contents
</a>
<a class="dropdown-item" href="{% url 'blog:search' %}">
<i class="fa fa-search mr-2"></i>
Search
</a>
{% if user.is_authenticated %}
<a class="dropdown-item" href="{% url 'blog:post_create' %}">
<i class="fa fa-pencil mr-2"></i>Create Post
Expand All @@ -50,30 +71,55 @@
</div>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'contacts:contact' %}"><i class="fa fa-envelope mr-2"></i>Contact</a>
<a class="nav-link" href="{% url 'contacts:contact' %}">
<i class="fa fa-envelope mr-2"></i>
Contact
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'pages:about_me' %}"><i class="fa fa-info-circle mr-2"></i>About Me</a>
<a class="nav-link" href="{% url 'pages:about_me' %}">
<i class="fa fa-info-circle mr-2"></i>
About Me
</a>
</li>
{% if user.is_authenticated %}
{% if user.profile.is_two_factor_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-user fa-fw mr-1"></i><strong>{{ user.user.display_name }}</strong>
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
>
<i class="fas fa-user fa-fw mr-1"></i><strong>{{ user.profile.display_name }}</strong>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{% url 'blog:users:profile' user.user.slug %}"><i class="fa fa-user-circle mr-2"></i>View Profile</a>
<a class="dropdown-item" href="{% url 'blog:users:profile_update' user.username %}"><i class="fa fa-pencil mr-2"></i>Update Profile</a>
<a class="dropdown-item" href="{% url 'blog:users:logout' %}"><i class="fa fa-sign-out mr-2"></i>Logout</a>
<a class="dropdown-item" href="{% url 'blog:users:profile' user.profile.slug %}">
<i class="fa fa-user-circle mr-2"></i>
View Profile
</a>
<a class="dropdown-item" href="{% url 'blog:users:profile_update' user.get_username %}">
<i class="fa fa-pencil mr-2"></i>
Update Profile
</a>
<a class="dropdown-item" href="{% url 'blog:users:logout' %}">
<i class="fa fa-sign-out mr-2"></i>
Logout
</a>
</div>
</li>
{% else %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
>
<i class="fas fa-lock fa-fw mr-1"></i>Account
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{% url 'blog:users:login' %}"><i class="fa fa-sign-in mr-2"></i>Login</a>
<a class="dropdown-item" href="{% url 'blog:users:register' %}"><i class="fa fa-pencil-square-o mr-2"></i>Register</a>
<a class="dropdown-item" href="{% url 'blog:users:login' %}">
<i class="fa fa-sign-in mr-2"></i>
Login
</a>
<a class="dropdown-item" href="{% url 'blog:users:register' %}">
<i class="fa fa-pencil-square-o mr-2"></i>
Register
</a>
</div>
</li>
{% endif %}
Expand All @@ -90,4 +136,4 @@
</div>
</nav>
</div>
</header>
</header>
Loading

0 comments on commit 0c4c86e

Please sign in to comment.