From 87c0578b368c7abbcc807c02ebb7a746fe74aa92 Mon Sep 17 00:00:00 2001
From: Dan Braghis <dan.braghis@torchbox.com>
Date: Thu, 25 Jan 2018 13:36:12 +0000
Subject: [PATCH] Handle account activation

---
 .../templates/users/activation/email.txt      | 20 ++++++
 .../users/activation/email_subject.txt        |  1 +
 .../templates/users/activation/invalid.html   |  7 ++
 opentech/apply/users/urls.py                  |  7 +-
 opentech/apply/users/utils.py                 | 25 +++++++
 opentech/apply/users/views.py                 | 67 +++++++++++++++++--
 6 files changed, 122 insertions(+), 5 deletions(-)
 create mode 100644 opentech/apply/users/templates/users/activation/email.txt
 create mode 100644 opentech/apply/users/templates/users/activation/email_subject.txt
 create mode 100644 opentech/apply/users/templates/users/activation/invalid.html

diff --git a/opentech/apply/users/templates/users/activation/email.txt b/opentech/apply/users/templates/users/activation/email.txt
new file mode 100644
index 000000000..3f01c1865
--- /dev/null
+++ b/opentech/apply/users/templates/users/activation/email.txt
@@ -0,0 +1,20 @@
+{% load wagtailadmin_tags %}{% base_url_setting as base_url %}
+Dear {{ name|default:username }},
+
+An account on Open Technology Fund has been created. Activate your account by clicking this link or copying and pasting it to your browser:
+
+{% if base_url %}{{ base_url }}{% else %}{{ protocol }}://{{ domain }}{% endif %}{% url 'users:activate' uidb64=uid token=token %}
+
+This link can be used once to log in and will lead you to a page where you can set your password.
+
+After setting your password, you will be able to log in at {% if base_url %}{{ base_url }}{% else %}{{ protocol }}://{{ domain }}{% endif %} in the future using:
+
+username: {{ username }}
+password: Your chosen password
+
+Thanks,
+The OTF Team
+
+--
+Open Technology Fund
+https://www.opentech.fund/
diff --git a/opentech/apply/users/templates/users/activation/email_subject.txt b/opentech/apply/users/templates/users/activation/email_subject.txt
new file mode 100644
index 000000000..1f1a8daaf
--- /dev/null
+++ b/opentech/apply/users/templates/users/activation/email_subject.txt
@@ -0,0 +1 @@
+Account details for {{ username }} at Open Technology Fund
diff --git a/opentech/apply/users/templates/users/activation/invalid.html b/opentech/apply/users/templates/users/activation/invalid.html
new file mode 100644
index 000000000..df12380a7
--- /dev/null
+++ b/opentech/apply/users/templates/users/activation/invalid.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+
+{% block title %}Invalid activation{% endblock %}
+
+{% block content %}
+    <h2>Invalid activation URL</h2>
+{% endblock %}
diff --git a/opentech/apply/users/urls.py b/opentech/apply/users/urls.py
index 1fc074d79..76a7b0563 100644
--- a/opentech/apply/users/urls.py
+++ b/opentech/apply/users/urls.py
@@ -2,7 +2,7 @@ from django.conf.urls import url
 from django.contrib.auth import views as auth_views
 from django.urls import reverse_lazy
 
-from opentech.apply.users.views import account, oauth
+from opentech.apply.users.views import account, oauth, ActivationView
 
 urlpatterns = [
     url(r'^$', account, name='account'),
@@ -53,5 +53,10 @@ urlpatterns = [
         auth_views.PasswordResetCompleteView.as_view(template_name='users/password_reset/complete.html'),
         name='password_reset_complete'
     ),
+    url(
+        r'^activate/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
+        ActivationView.as_view(),
+        name='activate'
+    ),
     url(r'^oauth$', oauth, name='oauth'),
 ]
diff --git a/opentech/apply/users/utils.py b/opentech/apply/users/utils.py
index 0e290b320..7f4ae6d4c 100644
--- a/opentech/apply/users/utils.py
+++ b/opentech/apply/users/utils.py
@@ -1,4 +1,8 @@
 from django.conf import settings
+from django.contrib.auth.tokens import PasswordResetTokenGenerator
+from django.template.loader import render_to_string
+from django.utils.encoding import force_bytes
+from django.utils.http import urlsafe_base64_encode
 
 
 def can_use_oauth_check(user):
@@ -14,3 +18,24 @@ def can_use_oauth_check(user):
         # Anonymous user or setting not defined
         pass
     return False
+
+
+def send_activation_email(user):
+    """
+    Send the activation email. The activation key is the username,
+    signed using TimestampSigner.
+    """
+    token_generator = PasswordResetTokenGenerator()
+    context = {
+        'user': user,
+        'name': user.get_full_name(),
+        'username': user.get_username(),
+        'uid': urlsafe_base64_encode(force_bytes(user.pk)),
+        'token': token_generator.make_token(user),
+    }
+
+    subject = render_to_string('users/activation/email_subject.txt', context)
+    # Force subject to a single line to avoid header-injection issues.
+    subject = ''.join(subject.splitlines())
+    message = render_to_string('users/activation/email.txt', context)
+    user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
diff --git a/opentech/apply/users/views.py b/opentech/apply/users/views.py
index c9bad14df..893b203c1 100644
--- a/opentech/apply/users/views.py
+++ b/opentech/apply/users/views.py
@@ -1,7 +1,12 @@
+from django.contrib.auth import get_user_model, login
 from django.contrib.auth.decorators import login_required
-from django.shortcuts import render
+from django.contrib.auth.tokens import PasswordResetTokenGenerator
+from django.shortcuts import redirect, render
 from django.template.response import TemplateResponse
 from django.urls import reverse_lazy
+from django.utils.encoding import force_text
+from django.utils.http import urlsafe_base64_decode
+from django.views.generic.base import TemplateView
 
 from wagtail.wagtailadmin.views.account import password_management_enabled
 
@@ -10,7 +15,7 @@ from .decorators import require_oauth_whitelist
 
 @login_required(login_url=reverse_lazy('users:login'))
 def account(request):
-    "Account page placeholder view"
+    """Account page placeholder view"""
 
     return render(request, 'users/account.html', {
         'show_change_password': password_management_enabled() and request.user.has_usable_password(),
@@ -20,8 +25,62 @@ def account(request):
 @login_required(login_url=reverse_lazy('users:login'))
 @require_oauth_whitelist
 def oauth(request):
+    """Generic, empty view for the OAuth associations."""
+
+    return TemplateResponse(request, 'users/oauth.html', {})
+
+
+class ActivationView(TemplateView):
     """
-    Generic, empty view for the OAuth associations.
+    Inspired by https://github.com/ubernostrum/django-registration
     """
 
-    return TemplateResponse(request, 'users/oauth.html', {})
+    def get(self, request, *args, **kwargs):
+        user = self.activate(*args, **kwargs)
+        if user:
+            user.backend = 'django.contrib.auth.backends.ModelBackend'
+            login(request, user)
+            return redirect('users:password_change')
+
+        return render(request, 'users/activation/invalid.html')
+
+    def activate(self, *args, **kwargs):
+        user = self.validate_token(kwargs.get('uidb64'), kwargs.get('token'))
+        if user:
+            user.is_active = True
+            user.save()
+            return user
+        return False
+
+    def validate_token(self, uidb64, token):
+        """
+        Verify that the activation key is valid and within the
+        permitted activation time window, returning the username if
+        valid or ``None`` if not.
+        """
+
+        uid = force_text(urlsafe_base64_decode(uidb64))
+        user = self.get_user(uid)
+        token_generator = PasswordResetTokenGenerator()
+
+        if user is not None and token_generator.check_token(user, token):
+            return user
+
+        return False
+
+    def get_user(self, uid):
+        """
+        Given the verified uid, look up and return the
+        corresponding user account if it exists, or ``None`` if it
+        doesn't.
+        """
+        User = get_user_model()
+
+        try:
+            user = User.objects.get(**{
+                'pk': uid,
+                'is_active': False
+            })
+            return user
+        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
+            return None
-- 
GitLab