diff --git a/demo/demo/accounts/templates/rest_auth_toolkit/email_confirmation.html b/demo/demo/accounts/templates/rest_auth_toolkit/email_confirmation.html
new file mode 100644
index 0000000..42f3464
--- /dev/null
+++ b/demo/demo/accounts/templates/rest_auth_toolkit/email_confirmation.html
@@ -0,0 +1,25 @@
+{% load i18n demotags %}
+
+
+
+
+
+
+
+
+
+
+ {% trans "Follow this link to validate your email:" %}
+ {% url "pages:confirm-email" token=confirmation.external_id as confirmation_url %}
+ {% frontend_base_url as base_url %}
+ {{ base_url }}{{ confirmation_url }}
+
+
+
+ {% trans "Or send an API request to simulate a front-end application:" %}
+ HTTP POST {{ base_url }}{% url "auth:confirm" %} email="{{ user.email }}" token="{{ confirmation.external_id }}"
+
+
+
+
+
diff --git a/demo/demo/accounts/templates/rest_auth_toolkit/email_confirmation.txt b/demo/demo/accounts/templates/rest_auth_toolkit/email_confirmation.txt
new file mode 100644
index 0000000..51d09b7
--- /dev/null
+++ b/demo/demo/accounts/templates/rest_auth_toolkit/email_confirmation.txt
@@ -0,0 +1,11 @@
+{% autoescape off %}
+{% load i18n demotags %}
+
+{% trans "Follow this link to validate your email:" %}
+{% frontend_base_url as base_url %}
+{{ base_url }}{% url "pages:confirm-email" token=confirmation.external_id %}
+
+{% trans "Or send an API request to simulate a front-end application:" %}
+ HTTP POST {{ base_url }}{% url "auth:confirm" %} email="{{ user.email }}" token="{{ confirmation.external_id }}"
+
+{% endautoescape %}
diff --git a/demo/demo/accounts/templatetags/__init__.py b/demo/demo/accounts/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/demo/demo/accounts/templatetags/demotags.py b/demo/demo/accounts/templatetags/demotags.py
new file mode 100644
index 0000000..3dd9a11
--- /dev/null
+++ b/demo/demo/accounts/templatetags/demotags.py
@@ -0,0 +1,10 @@
+from django import template
+
+
+register = template.Library()
+
+
+@register.simple_tag(takes_context=True)
+def frontend_base_url(context):
+ request = context['request']
+ return request.build_absolute_uri('/')[:-1]
diff --git a/demo/demo/pages/auth_urls.py b/demo/demo/pages/auth_urls.py
deleted file mode 100644
index 6979f2a..0000000
--- a/demo/demo/pages/auth_urls.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from django.urls import path
-
-from .views import email_view
-
-
-app_name = 'app-auth'
-
-urlpatterns = [
- path('emails//', email_view, name='email-confirmation'),
-]
diff --git a/demo/demo/pages/templates/base.html b/demo/demo/pages/templates/base.html
new file mode 100644
index 0000000..88bf32e
--- /dev/null
+++ b/demo/demo/pages/templates/base.html
@@ -0,0 +1,13 @@
+{% load i18n %}
+{% get_current_language as LANGUAGE_CODE %}
+
+
+
+
+
+ {% block title %}{% endblock %}
+
+
+ {% block body %}{% endblock %}
+
+
diff --git a/demo/demo/pages/templates/error.html b/demo/demo/pages/templates/error.html
index cb2a99e..a791ac2 100644
--- a/demo/demo/pages/templates/error.html
+++ b/demo/demo/pages/templates/error.html
@@ -1,18 +1,10 @@
+{% extends "base.html" %}
{% load i18n %}
-{% get_current_language as LANGUAGE_CODE %}
-
-
-
-
-
- {{ site_name }}
-
-
+{% block title %}Error! {{ site_name }}{% endblock %}
+{% block body %}
{% trans "Error!" %}
{{ error }}
-
-
-
+{% endblock %}
diff --git a/demo/demo/pages/templates/index.html b/demo/demo/pages/templates/index.html
index 2d42d3a..3022ceb 100644
--- a/demo/demo/pages/templates/index.html
+++ b/demo/demo/pages/templates/index.html
@@ -1,13 +1,7 @@
-{% load i18n %}
-{% get_current_language as LANGUAGE_CODE %}
-
-
-
-
-
- {{ site_name }}
-
-
+{% extends "base.html" %}
+{% block title %}{{ site_name }}{% endblock %}
+
+{% block body %}
-
-
+{% endblock %}
diff --git a/demo/demo/pages/templates/welcome.html b/demo/demo/pages/templates/welcome.html
new file mode 100644
index 0000000..e21a569
--- /dev/null
+++ b/demo/demo/pages/templates/welcome.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% block title %}Success! {{ site_name }}{% endblock %}
+
+{% block body %}
+
+
{% trans "Success!" %}
+
Your address {{ email }} is now confirmed.
+
+{% endblock %}
diff --git a/demo/demo/pages/urls.py b/demo/demo/pages/urls.py
index 90b65eb..8caefe1 100644
--- a/demo/demo/pages/urls.py
+++ b/demo/demo/pages/urls.py
@@ -8,4 +8,5 @@
urlpatterns = [
path('', views.index, name='root'),
+ path('welcome//', views.confirm_email, name='confirm-email'),
]
diff --git a/demo/demo/pages/views.py b/demo/demo/pages/views.py
index 7c95058..c0fd954 100644
--- a/demo/demo/pages/views.py
+++ b/demo/demo/pages/views.py
@@ -14,12 +14,12 @@ def index(request):
return render(request, 'index.html', context=ctx)
-def email_view(request, external_id):
+def confirm_email(request, token):
"""Landing page for links in confirmation emails."""
error = None
try:
- confirmation = EmailConfirmation.objects.get(external_id=external_id)
+ confirmation = EmailConfirmation.objects.get(external_id=token)
confirmation.confirm()
except EmailConfirmation.DoesNotExist:
error = _('Invalid link')
@@ -33,4 +33,8 @@ def email_view(request, external_id):
}
return render(request, 'error.html', context=ctx)
else:
- return index(request)
+ ctx = {
+ 'site_name': 'Demo',
+ 'email': confirmation.user.email,
+ }
+ return render(request, 'welcome.html', context=ctx)
diff --git a/demo/demo/settings.py b/demo/demo/settings.py
index b1ceb25..c70275e 100644
--- a/demo/demo/settings.py
+++ b/demo/demo/settings.py
@@ -121,6 +121,5 @@
REST_AUTH_TOOLKIT = {
'email_confirmation_class': 'demo.accounts.models.EmailConfirmation',
'email_confirmation_from': 'auth-demo@localhost',
- 'email_confirmation_lookup_field': 'external_id',
'api_token_class': 'demo.accounts.models.APIToken',
}
diff --git a/demo/demo/urls.py b/demo/demo/urls.py
index 8c03624..6a998be 100644
--- a/demo/demo/urls.py
+++ b/demo/demo/urls.py
@@ -6,7 +6,13 @@
from rest_framework.documentation import include_docs_urls
from rest_framework.renderers import DocumentationRenderer
-from rest_auth_toolkit.views import FacebookLoginView, LoginView, LogoutView, SignupView
+from rest_auth_toolkit.views import (
+ EmailConfirmationView,
+ FacebookLoginView,
+ LoginView,
+ LogoutView,
+ SignupView,
+)
class GoAwayRenderer(DocumentationRenderer):
@@ -16,6 +22,7 @@ class GoAwayRenderer(DocumentationRenderer):
auth_urlpatterns = [
path('signup/', SignupView.as_view(), name='signup'),
+ path('confirm/', EmailConfirmationView.as_view(), name='confirm'),
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
path('fb-login/', FacebookLoginView.as_view(), name='fb-login'),
@@ -32,5 +39,4 @@ class GoAwayRenderer(DocumentationRenderer):
path('admin/', admin.site.urls),
path('api/', include(api_urlpatterns)),
path('', include('demo.pages.urls')),
- path('', include('demo.pages.auth_urls')),
]
diff --git a/rest_auth_toolkit/app.py b/rest_auth_toolkit/app.py
index 594ce45..48edb28 100644
--- a/rest_auth_toolkit/app.py
+++ b/rest_auth_toolkit/app.py
@@ -5,7 +5,7 @@ class RestAuthToolkitConfig(AppConfig):
"""Default app config for RATK.
This installs a signal handler to set user.is_active when
- email_confirmed is emitted.
+ email_confirmed is emitted by EmailConfirmationView.
"""
name = 'rest_auth_toolkit'
diff --git a/rest_auth_toolkit/serializers.py b/rest_auth_toolkit/serializers.py
index dc7dfbe..a895834 100644
--- a/rest_auth_toolkit/serializers.py
+++ b/rest_auth_toolkit/serializers.py
@@ -8,6 +8,8 @@
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
+from .utils import get_object_from_setting
+
try:
import facepy
except ImportError:
@@ -17,6 +19,7 @@
User = get_user_model()
+EmailConfirmation = get_object_from_setting('email_confirmation_class')
class SignupDeserializer(serializers.ModelSerializer):
@@ -54,6 +57,31 @@ def create(self, validated_data):
)
+class EmailConfirmationDeserializer(serializers.Serializer):
+ email = serializers.EmailField()
+ token = serializers.CharField()
+
+ def validate(self, data):
+ msg = None
+
+ try:
+ confirmation = EmailConfirmation.objects.get(
+ external_id=data['token'],
+ user__email=data['email'],
+ )
+ confirmation.confirm()
+ except EmailConfirmation.DoesNotExist:
+ msg = _('Invalid link')
+ except EmailConfirmation.IsExpired:
+ # FIXME it's not possible to register with the same email
+ msg = _('Email expired, please register again')
+
+ if msg:
+ raise ValidationError({'errors': [msg]})
+
+ return {'user': confirmation.user}
+
+
class LoginDeserializer(serializers.Serializer):
"""Deserializer to find a user from credentials."""
diff --git a/rest_auth_toolkit/templates/rest_auth_toolkit/email_confirmation.html b/rest_auth_toolkit/templates/rest_auth_toolkit/email_confirmation.html
deleted file mode 100644
index 3ad3539..0000000
--- a/rest_auth_toolkit/templates/rest_auth_toolkit/email_confirmation.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{% load i18n %}
-
-
-
-
-
-
-
-
-
-
diff --git a/rest_auth_toolkit/templates/rest_auth_toolkit/email_confirmation.txt b/rest_auth_toolkit/templates/rest_auth_toolkit/email_confirmation.txt
deleted file mode 100644
index 22a01d5..0000000
--- a/rest_auth_toolkit/templates/rest_auth_toolkit/email_confirmation.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-{% autoescape off %}
-{% load i18n %}
-
-{% trans "Follow this link to validate your email:" %}
-{{ confirmation_url }}
-
-{% endautoescape %}
diff --git a/rest_auth_toolkit/views.py b/rest_auth_toolkit/views.py
index 011b433..064c14a 100644
--- a/rest_auth_toolkit/views.py
+++ b/rest_auth_toolkit/views.py
@@ -1,14 +1,15 @@
from django.contrib.auth import get_user_model
from django.core.mail import send_mail
from django.template.loader import render_to_string
-from django.urls import reverse
from django.utils.translation import gettext as _
from rest_framework import generics, status, views
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
-from .serializers import FacebookLoginDeserializer, LoginDeserializer, SignupDeserializer
+from .models import email_confirmed
+from .serializers import (FacebookLoginDeserializer, LoginDeserializer,
+ SignupDeserializer, EmailConfirmationDeserializer)
from .utils import get_object_from_setting, get_setting, MissingSetting
try:
@@ -26,11 +27,27 @@ class SignupView(generics.GenericAPIView):
If the setting email_confirmation_send_email is true (default),
the function send_email will be called. That function requires
- that your app define a route named app-auth:email-confirmation
- with an id parameter; the view for this route should get an
- email confirmation instance using the ID and call the confirm
- method. To use a field that's not named 'id', define the setting
- email_confirmation_lookup_param (this will change the URL pattern).
+ that your project defines defines two email templates:
+ - rest_auth_toolkit/email_confirmation.txt
+ - rest_auth_toolkit/email_confirmation.html
+
+ The templates will be passed the User and EmailConfirmation instances
+ (as variables *user* and *confirmation*) as well as the request;
+ note that template context processors are not available in email
+ teamplates.
+
+ It is up to your project to generate a link that will work, using
+ your own template code or custom tags.
+
+ The demo app shows one way to handle this: a Django view validates
+ the email confirmation token in its URL; the link is generated with
+ a custom template tag because Django doesn't offer a tag to create
+ full URLs. For a project with a front-end site (e.g. a JavaScript app)
+ on a different domain than the API powered by Django, the template tag
+ could for example use a setting to know the front-end domain name + a
+ mapping of front-end routes to generate the path portion of the links.
+
+ If the setting is false, the user will be active immediately.
"""
authentication_classes = ()
permission_classes = ()
@@ -48,19 +65,71 @@ def post(self, request):
"""
deserializer = self.get_serializer(data=request.data)
deserializer.is_valid(raise_exception=True)
- user = deserializer.save()
- if self.email_confirmation_class is None:
- raise MissingSetting('email_confirmation_string')
+ confirm_email = get_setting('email_confirmation_send_email', True)
- confirmation = self.email_confirmation_class.objects.create(user=user)
- if get_setting('email_confirmation_send_email', True):
+ if not confirm_email:
+ deserializer.save(is_active=True)
+ else:
+ user = deserializer.save()
+
+ if self.email_confirmation_class is None:
+ raise MissingSetting('email_confirmation_class')
+
+ confirmation = self.email_confirmation_class.objects.create(user=user)
email_field = user.get_email_field_name()
send_email(request, user, getattr(user, email_field), confirmation)
return Response(deserializer.data, status=status.HTTP_201_CREATED)
+def send_email(request, user, address, confirmation):
+ """Send the confirmation email for a new user."""
+ subject = _('Confirm your email address')
+ from_address = get_setting('email_confirmation_from')
+
+ context = {
+ 'user': user,
+ 'confirmation': confirmation,
+ }
+ txt_content = render_to_string('rest_auth_toolkit/email_confirmation.txt',
+ context, request=request)
+ html_content = render_to_string('rest_auth_toolkit/email_confirmation.html',
+ context, request=request)
+
+ send_mail(subject=subject,
+ from_email=from_address, recipient_list=[address],
+ message=txt_content, html_message=html_content,
+ fail_silently=False)
+
+
+class EmailConfirmationView(generics.GenericAPIView):
+ """Validate an email address after sign-up.
+
+ Response
+
+ `200 OK`
+
+ Error response (code 400):
+
+ ```json
+ {"errors": {"token": "Error message"}}
+ ```
+ """
+ authentication_classes = ()
+ permission_classes = ()
+ serializer_class = get_object_from_setting('email_confirmation_serializer_class',
+ EmailConfirmationDeserializer)
+
+ def post(self, request):
+ deserializer = self.get_serializer(data=request.data)
+ deserializer.is_valid(raise_exception=True)
+
+ email_confirmed.send(sender=self.__class__,
+ user=deserializer.validated_data['user'])
+ return Response()
+
+
class LoginView(generics.GenericAPIView):
"""Email address log-in endpoint.
@@ -145,28 +214,6 @@ def post(self, request):
return Response(status=status.HTTP_200_OK)
-def send_email(request, user, address, confirmation):
- """Send the confirmation email for a new user."""
- subject = _('Confirm your email address')
- from_address = get_setting('email_confirmation_from')
-
- lookup_field = get_setting('email_confirmation_lookup_field', 'id')
- confirmation_url = request.build_absolute_uri(
- reverse('app-auth:email-confirmation',
- kwargs={lookup_field: getattr(confirmation, lookup_field)}))
- # The url template tag doesn't include scheme/domain/port, pass a helper
- base_url = request.build_absolute_uri('/')[:-1]
-
- context = {'base_url': base_url, 'confirmation_url': confirmation_url}
- txt_content = render_to_string('rest_auth_toolkit/email_confirmation.txt', context)
- html_content = render_to_string('rest_auth_toolkit/email_confirmation.html', context)
-
- send_mail(subject=subject,
- from_email=from_address, recipient_list=[address],
- message=txt_content, html_message=html_content,
- fail_silently=False)
-
-
def activate_user(sender, **kwargs):
"""Mark user as active when a confirmation link is visited.
diff --git a/tests/functional/test_email.py b/tests/functional/test_email.py
index b5fe5b3..e48e4f9 100644
--- a/tests/functional/test_email.py
+++ b/tests/functional/test_email.py
@@ -67,8 +67,8 @@ def test_confirm_email(db, django_app, user0, emailconfirmation0):
assert not user0.is_active
assert emailconfirmation0.confirmed is None
- django_app.get(reverse("app-auth:email-confirmation",
- kwargs={"external_id": emailconfirmation0.external_id}))
+ django_app.get(reverse("pages:confirm-email",
+ kwargs={"token": emailconfirmation0.external_id}))
user0.refresh_from_db()
emailconfirmation0.refresh_from_db()