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