Skip to content

Commit

Permalink
feat: Allow for using links over raw tokens.
Browse files Browse the repository at this point in the history
Instead of providing the raw tokens for email verifications and password
resets, users can now configure the app to send links to other web
pages.

Closes #32
  • Loading branch information
cdriehuys committed Oct 6, 2019
1 parent 4dc44d5 commit 2abcd00
Show file tree
Hide file tree
Showing 15 changed files with 299 additions and 7 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
Changelog
#########

**************
In Development
**************

Features
========

* :issue:`32`: Link to frontend pages for resetting passwords and verifying
email addresses. These links are specified with the ``PASSWORD_RESET_URL`` and
``EMAIL_VERIFICATION_URL`` settings, respectively.

******
v0.3.1
******
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:caption: Contents:

installation
settings
templates
changelog-proxy

Expand Down
8 changes: 8 additions & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ Add ``email_auth`` to your ``INSTALLED_APPS``:
# Django apps
# Third party apps
# Core models and templates:
"email_auth",
# If you would like to use the provided REST API:
"email_auth.interfaces.rest",
# More third party apps
# Your custom apps
Expand All @@ -47,5 +53,7 @@ ensure ``DEFAULT_FROM_EMAIL`` is set. This is the address that all account
related emails such as email verifications and password reset emails are sent
from.

See :ref:`app-settings` for configuration options.

.. _django-emails: https://docs.djangoproject.com/en/dev/topics/email/
.. _django-simple-email-auth-pypi: https://pypi.org/project/django-simple-email-auth/
45 changes: 45 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.. _app-settings:

########
Settings
########

Settings are provided as a dictionary named ``EMAIL_AUTH`` in the Django
settings file. For example::

# settings.py

EMAIL_AUTH = {
"EMAIL_VERIFICATION_URL": "https://example.com/{key}"
}

.. _email-verification-url:

**************************
``EMAIL_VERIFICATION_URL``
**************************

Default
``None``

Example
``https://my-frontend.com/verify-email/{key}``

A template used to construct the URL of the page that users visit to verify
their email. The placeholder ``{key}`` will be replaced with the verification
token.

.. _password-reset-url:

**********************
``PASSWORD_RESET_URL``
**********************

Default
``None``

Example
``https://my-frontend.com/reset-password/{key}``

A template used to construct the URL of the page that users visit to reset their
password. The placeholder ``{key}`` will be replaced with the reset token.
8 changes: 8 additions & 0 deletions docs/templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ Provided Context
``verification``
The ``EmailVerification`` instance containing the token used to verify
ownership of the email address.
``verification_url``
If the :ref:`email-verification-url` setting has been set, this variable
contains the provided URL formatted with the verification token. If the
setting was not provided, this is ``None``.

**************
Password Reset
Expand All @@ -86,6 +90,10 @@ Provided Context
``password_reset``
The ``PasswordReset`` instance containing the token used to reset the user's
password.
``reset_url``
If the :ref:`password-reset-url` setting has been set, this variable
contains the provided template formatted with the reset token. If the
setting was not provided, this is ``None``.


.. _django-email-utils: https://github.com/cdriehuys/django-email-utils
56 changes: 56 additions & 0 deletions email_auth/app_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
Settings specific to ``simple_email_auth``.
The setting implementation is modeled on "Django Allauth's" from:
https://github.com/pennersr/django-allauth/blob/master/allauth/account/app_settings.py
"""

import sys
from typing import Optional


class AppSettings(object):
def _setting(self, name: str, default: any):
"""
Retrieve a setting from the current Django settings.
Settings are retrieved from the ``EMAIL_AUTH`` dict in the
settings file.
Args:
name:
The name of the setting to retrieve.
default:
The setting's default value.
Returns:
The value provided in the settings dictionary if it exists.
The default value is returned otherwise.
"""
from django.conf import settings

settings_dict = getattr(settings, "EMAIL_AUTH", {})

return settings_dict.get(name, default)

@property
def EMAIL_VERIFICATION_URL(self) -> Optional[str]:
"""
The template to use for the email verification url.
"""
return self._setting("EMAIL_VERIFICATION_URL", None)

@property
def PASSWORD_RESET_URL(self) -> Optional[str]:
"""
The template to use for the password reset url.
"""
return self._setting("PASSWORD_RESET_URL", None)


# Ugly? Guido recommends this himself ...
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html

app_settings = AppSettings()
app_settings.__name__ = __name__
sys.modules[__name__] = app_settings
Empty file.
3 changes: 3 additions & 0 deletions email_auth/interfaces/rest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
default_app_config = (
"email_auth.interfaces.rest.apps.EmailAuthRESTInterfaceConfig"
)
11 changes: 11 additions & 0 deletions email_auth/interfaces/rest/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _


class EmailAuthRESTInterfaceConfig(AppConfig):
"""
Default configuration for ``email_auth.interfaces.rest``.
"""

name = "email_auth.interfaces.rest"
verbose_name = _("Simple Email Authentication REST Interface")
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% load i18n %}{% blocktrans with user=password_reset.email.user %}Hello {{ user }},

Please reset your password using the following link:

{{ reset_url }}
{% endblocktrans %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% load i18n %}{% blocktrans %}Hello {{ user }},

Please visit the following URL to verify your email address:

{{ verification_url }}
{% endblocktrans %}
17 changes: 16 additions & 1 deletion email_auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from django.utils import crypto, timezone
from django.utils.translation import ugettext_lazy as _

from email_auth import app_settings


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -256,10 +258,17 @@ def send_email(self):
Send an email containing the verification token to the email
address being verified.
"""
verification_url_template = app_settings.EMAIL_VERIFICATION_URL
if verification_url_template is not None:
verification_url = verification_url_template.format(key=self.token)
else:
verification_url = None

context = {
"email": self.email,
"user": self.email.user,
"verification": self,
"verification_url": verification_url,
}
template = "email_auth/emails/verify-email"

Expand Down Expand Up @@ -350,7 +359,13 @@ def send_email(self):
Send the token authorizing the password reset to the email
address associated with the instance.
"""
context = {"password_reset": self}
reset_url_template = app_settings.PASSWORD_RESET_URL
if reset_url_template is not None:
reset_url = reset_url_template.format(key=self.token)
else:
reset_url = None

context = {"password_reset": self, "reset_url": reset_url}

email_utils.send_email(
context=context,
Expand Down
43 changes: 41 additions & 2 deletions email_auth/test/models/test_email_verification_model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from unittest import mock

from django.conf import settings
from django.conf import settings as django_settings
from django.contrib.auth import get_user_model
from django.utils import timezone

Expand Down Expand Up @@ -57,8 +57,47 @@ def test_send_email(mock_now, mock_send_email, _):
"email": email,
"user": user,
"verification": verification,
"verification_url": None,
},
"from_email": settings.DEFAULT_FROM_EMAIL,
"from_email": django_settings.DEFAULT_FROM_EMAIL,
"recipient_list": [email.address],
"subject": "Please Verify Your Email Address",
"template_name": "email_auth/emails/verify-email",
}

assert verification.time_sent == mock_now.return_value
assert verification.save.call_count == 1


@mock.patch("email_auth.models.EmailVerification.save", autospec=True)
@mock.patch("email_auth.models.email_utils.send_email", autospec=True)
@mock.patch("email_auth.models.timezone.now", autospec=True)
def test_send_email_with_verification_url(
mock_now, mock_send_email, _, settings
):
"""
This method should send the email verification token to the
associated email address and record the send time of the email.
"""
verification_url = "example.com/verify/{key}"
settings.EMAIL_AUTH = {"EMAIL_VERIFICATION_URL": verification_url}

user = get_user_model()()
email = models.EmailAddress(address="test@example.com", user=user)
verification = models.EmailVerification(email=email)

verification.send_email()

assert mock_send_email.call_args[1] == {
"context": {
"email": email,
"user": user,
"verification": verification,
"verification_url": verification_url.format(
key=verification.token
),
},
"from_email": django_settings.DEFAULT_FROM_EMAIL,
"recipient_list": [email.address],
"subject": "Please Verify Your Email Address",
"template_name": "email_auth/emails/verify-email",
Expand Down
39 changes: 35 additions & 4 deletions email_auth/test/models/test_password_reset_model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from unittest import mock

from django.conf import settings
from django.conf import settings as django_settings
from django.contrib.auth import get_user_model
from django.utils import timezone

Expand Down Expand Up @@ -43,7 +43,7 @@ def test_repr():
@mock.patch("email_auth.models.PasswordReset.save", autospec=True)
@mock.patch("email_auth.models.email_utils.send_email", autospec=True)
@mock.patch("email_auth.models.timezone.now", autospec=True)
def test_send_email(mock_now, mock_send_email, mock_save):
def test_send_email(mock_now, mock_send_email, _):
"""
This method should send the password reset token to the associated
email address and record the send time of the email.
Expand All @@ -54,8 +54,39 @@ def test_send_email(mock_now, mock_send_email, mock_save):
password_reset.send_email()

assert mock_send_email.call_args[1] == {
"context": {"password_reset": password_reset},
"from_email": settings.DEFAULT_FROM_EMAIL,
"context": {"password_reset": password_reset, "reset_url": None},
"from_email": django_settings.DEFAULT_FROM_EMAIL,
"recipient_list": [email.address],
"subject": "Reset Your Password",
"template_name": "email_auth/emails/reset-password",
}

assert password_reset.time_sent == mock_now.return_value
assert password_reset.save.call_count == 1


@mock.patch("email_auth.models.PasswordReset.save", autospec=True)
@mock.patch("email_auth.models.email_utils.send_email", autospec=True)
@mock.patch("email_auth.models.timezone.now", autospec=True)
def test_send_email_with_reset_url(mock_now, mock_send_email, _, settings):
"""
This method should send the password reset token to the associated
email address and record the send time of the email.
"""
reset_url_template = "example.com/reset-password/{key}"
settings.EMAIL_AUTH = {"PASSWORD_RESET_URL": reset_url_template}

email = models.EmailAddress(address="test@example.com")
password_reset = models.PasswordReset(email=email)

password_reset.send_email()

assert mock_send_email.call_args[1] == {
"context": {
"password_reset": password_reset,
"reset_url": reset_url_template.format(key=password_reset.token),
},
"from_email": django_settings.DEFAULT_FROM_EMAIL,
"recipient_list": [email.address],
"subject": "Reset Your Password",
"template_name": "email_auth/emails/reset-password",
Expand Down

0 comments on commit 2abcd00

Please sign in to comment.