From ad96f7dbb8eb37bcfd87c683a2747ff9ea4ab46f Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 26 Feb 2019 14:07:47 +0100 Subject: [PATCH] add E-Mail support via celery task, untested, closes #17 --- .gitlab-ci.yml | 1 - passbook/core/auth/factors/password.py | 14 +- passbook/core/tasks.py | 17 +++ .../core/templates/email/account_confirm.html | 84 ++++++++++++ .../email/account_password_reset.html | 78 +++++++++++ passbook/core/templates/email/base.html | 129 ++++++++++++++++++ .../core/templates/email/generic_email.html | 26 ++++ passbook/core/views/authentication.py | 14 +- passbook/ldap/forms.py | 8 +- passbook/ldap/models.py | 2 +- passbook/ldap/urls.py | 2 +- passbook/ldap/views.py | 6 +- passbook/lib/utils/inline.py | 20 +++ 13 files changed, 388 insertions(+), 13 deletions(-) create mode 100644 passbook/core/tasks.py create mode 100644 passbook/core/templates/email/account_confirm.html create mode 100644 passbook/core/templates/email/account_password_reset.html create mode 100644 passbook/core/templates/email/base.html create mode 100644 passbook/core/templates/email/generic_email.html create mode 100644 passbook/lib/utils/inline.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 004a2c9d3..f6903e6e9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,7 +16,6 @@ variables: POSTGRES_DB: passbook POSTGRES_USER: passbook POSTGRES_PASSWORD: 'EK-5jnKfjrGRm<77' - SUPERVISR_ENV: ci include: - /allauth/.gitlab-ci.yml diff --git a/passbook/core/auth/factors/password.py b/passbook/core/auth/factors/password.py index b9072bb08..136759517 100644 --- a/passbook/core/auth/factors/password.py +++ b/passbook/core/auth/factors/password.py @@ -5,7 +5,7 @@ from django.contrib import messages from django.contrib.auth import authenticate from django.core.exceptions import PermissionDenied from django.forms.utils import ErrorList -from django.shortcuts import redirect +from django.shortcuts import redirect, reverse from django.utils.translation import gettext as _ from django.views.generic import FormView @@ -13,6 +13,7 @@ from passbook.core.auth.factor import AuthenticationFactor from passbook.core.auth.view import AuthenticationView from passbook.core.forms.authentication import PasswordFactorForm from passbook.core.models import Nonce +from passbook.core.tasks import send_email from passbook.lib.config import CONFIG LOGGER = getLogger(__name__) @@ -32,7 +33,16 @@ class PasswordFactor(FormView, AuthenticationFactor): if 'password-forgotten' in request.GET: nonce = Nonce.objects.create(user=self.pending_user) LOGGER.debug("DEBUG %s", str(nonce.uuid)) - # TODO: Send email to user + # Send mail to user + send_email.delay(self.pending_user.email, _('Forgotten password'), + 'email/account_password_reset.html', { + 'url': self.request.build_absolute_uri( + reverse('passbook_core:passbook_core:auth-password-reset', + kwargs={ + 'nonce': nonce.uuid + }) + ) + }) self.authenticator.cleanup() messages.success(request, _('Check your E-Mails for a password reset link.')) return redirect('passbook_core:auth-login') diff --git a/passbook/core/tasks.py b/passbook/core/tasks.py new file mode 100644 index 000000000..6d390ecda --- /dev/null +++ b/passbook/core/tasks.py @@ -0,0 +1,17 @@ +"""passbook core tasks""" +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils.html import strip_tags + +from passbook.core.celery import CELERY_APP +from passbook.lib.config import CONFIG + + +@CELERY_APP.task() +def send_email(to_address, subject, template, context): + """Send Email to user(s)""" + html_content = render_to_string(template, context=context) + text_content = strip_tags(html_content) + msg = EmailMultiAlternatives(subject, text_content, CONFIG.y('email.from'), [to_address]) + msg.attach_alternative(html_content, "text/html") + msg.send() diff --git a/passbook/core/templates/email/account_confirm.html b/passbook/core/templates/email/account_confirm.html new file mode 100644 index 000000000..5a4488162 --- /dev/null +++ b/passbook/core/templates/email/account_confirm.html @@ -0,0 +1,84 @@ +{% extends 'email/base.html' %} + +{% load inline %} +{% load i18n %} + +{% block pre_header %} +{% trans "We're thrilled to have you here! Get ready to dive into your new account." %} +{% endblock %} + +{% block content %} + + + + + + + +
+

{% trans 'Welcome!' %} +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ {% trans "We're excited to have you get started. First, you need to confirm your account. Just press the button below."%} +

+
+ + + + +
+ + + + +
{% trans 'Confirm Account' %} +
+
+
+

+ {% trans "If that doesn't work, copy and paste the following link in your browser:" %}

+
+

{{ url }}

+
+

+ {% trans "If you have any questions, just reply to this email—we're always happy to help out." %} +

+
+ + +{% endblock %} diff --git a/passbook/core/templates/email/account_password_reset.html b/passbook/core/templates/email/account_password_reset.html new file mode 100644 index 000000000..c201e811e --- /dev/null +++ b/passbook/core/templates/email/account_password_reset.html @@ -0,0 +1,78 @@ +{% extends "email/base.html" %} + +{% load utils %} +{% load i18n %} + +{% block pre_header %} +{% trans "Looks like you tried signing in a few too many times. Let's see if we can get you back into your account." %} +{% endblock %} + +{% block content %} +{% config 'passbook.branding' as branding %} + + + + + + + +
+

{% trans 'Trouble signing in?' %}

+
+ + + + + + + + + + + + + + +
+

{% trans "Resetting your password is easy. Just press the button below and follow the instructions. We'll have you up and running in no time." %}

+
+ + + + +
+ + + + +
{% trans 'Reset Password' %}
+
+
+ + + + + + + + + + + + + + + + + + +
+

{% trans 'Want a more secure account?' %}

+
+

{% trans 'We support two-factor authentication to help keep your information private.' %}

+
+

{% trans 'See how easy it is to get started' %}

+
+ + +{% endblock %} diff --git a/passbook/core/templates/email/base.html b/passbook/core/templates/email/base.html new file mode 100644 index 000000000..d2038808c --- /dev/null +++ b/passbook/core/templates/email/base.html @@ -0,0 +1,129 @@ +{% load inline %} +{% load utils %} +{% load static %} +{% load i18n %} + + + + {% config passbook.branding %} + + + + + + + + + +
+ {% block pre_header %} + {% endblock %} +
+ + + + + + + {% block content %} + {% endblock %} + + + + + + +
+ + + + +
+ + Logo + +
+
+ + + + + + + + + +
+

+

+
+

{% config 'passbook.branding' %}

+
+
+ + diff --git a/passbook/core/templates/email/generic_email.html b/passbook/core/templates/email/generic_email.html new file mode 100644 index 000000000..915e16398 --- /dev/null +++ b/passbook/core/templates/email/generic_email.html @@ -0,0 +1,26 @@ +{% extends "email/base.html" %} + +{% block content %} + + + + + + +
+

{{ title }}!

+
+ + + + + + + + +
+

{{ body }}

+
+ + +{% endblock %} diff --git a/passbook/core/views/authentication.py b/passbook/core/views/authentication.py index 2a463dd6f..26621eb2f 100644 --- a/passbook/core/views/authentication.py +++ b/passbook/core/views/authentication.py @@ -15,10 +15,12 @@ from passbook.core.auth.view import AuthenticationView from passbook.core.forms.authentication import LoginForm, SignUpForm from passbook.core.models import Invitation, Nonce, Source, User from passbook.core.signals import invitation_used, user_signed_up +from passbook.core.tasks import send_email from passbook.lib.config import CONFIG LOGGER = getLogger(__name__) + class LoginView(UserPassesTestMixin, FormView): """Allow users to sign in""" @@ -76,6 +78,7 @@ class LoginView(UserPassesTestMixin, FormView): messages.error(request, _('Failed to authenticate.')) return self.render_to_response(self.get_context_data()) + class LogoutView(LoginRequiredMixin, View): """Log current user out""" @@ -145,7 +148,15 @@ class SignUpView(UserPassesTestMixin, FormView): if needs_confirmation: nonce = Nonce.objects.create(user=self._user) LOGGER.debug(str(nonce.uuid)) - # TODO: Send E-Mail to user + # Send email to user + send_email.delay(self._user.email, _('Confirm your account.'), + 'email/account_confirm.html', { + 'url': self.request.build_absolute_uri( + reverse('passbook_core:auth-sign-up-confirm', kwargs={ + 'nonce': nonce.uuid + }) + ) + }) self._user.is_active = False self._user.save() self.consume_invitation() @@ -196,6 +207,7 @@ class SignUpView(UserPassesTestMixin, FormView): request=request) return new_user + class SignUpConfirmView(View): """Confirm registration from Nonce""" diff --git a/passbook/ldap/forms.py b/passbook/ldap/forms.py index 62d9efeaf..cb47328c1 100644 --- a/passbook/ldap/forms.py +++ b/passbook/ldap/forms.py @@ -39,7 +39,7 @@ class LDAPSourceForm(forms.ModelForm): # (MODE_CREATE_USERS, _('Create Users')) # ) -# namespace = 'supervisr.mod.auth.ldap' +# namespace = 'passbook.ldap' # settings = ['enabled', 'mode'] # widgets = { @@ -51,7 +51,7 @@ class LDAPSourceForm(forms.ModelForm): # class ConnectionSettings(SettingsForm): # """Connection settings form""" -# namespace = 'supervisr.mod.auth.ldap' +# namespace = 'passbook.ldap' # settings = ['server', 'server:tls', 'bind:user', 'bind:password', 'domain'] # attrs_map = { @@ -68,7 +68,7 @@ class LDAPSourceForm(forms.ModelForm): # class AuthenticationBackendSettings(SettingsForm): # """Authentication backend settings""" -# namespace = 'supervisr.mod.auth.ldap' +# namespace = 'passbook.ldap' # settings = ['base'] # attrs_map = { @@ -79,7 +79,7 @@ class LDAPSourceForm(forms.ModelForm): # class CreateUsersSettings(SettingsForm): # """Create users settings""" -# namespace = 'supervisr.mod.auth.ldap' +# namespace = 'passbook.ldap' # settings = ['create_base'] # attrs_map = { diff --git a/passbook/ldap/models.py b/passbook/ldap/models.py index ca98c7276..6d112cfc7 100644 --- a/passbook/ldap/models.py +++ b/passbook/ldap/models.py @@ -57,7 +57,7 @@ class LDAPSource(Source): # class LDAPGroupMapping(UUIDModel, CreatedUpdatedModel): -# """Model to map an LDAP Group to a supervisr group""" +# """Model to map an LDAP Group to a passbook group""" # ldap_dn = models.TextField() # group = models.ForeignKey(Group, on_delete=models.CASCADE) diff --git a/passbook/ldap/urls.py b/passbook/ldap/urls.py index 13fc15a44..b643ba6bc 100644 --- a/passbook/ldap/urls.py +++ b/passbook/ldap/urls.py @@ -2,7 +2,7 @@ # from django.conf.urls import url -# from supervisr.mod.auth.ldap import views +# from passbook.mod.auth.ldap import views # urlpatterns = [ # url(r'^settings/$', views.admin_settings, name='admin_settings'), diff --git a/passbook/ldap/views.py b/passbook/ldap/views.py index 22255e8b5..5eb73d82e 100644 --- a/passbook/ldap/views.py +++ b/passbook/ldap/views.py @@ -1,4 +1,4 @@ -# """Supervisr Mod LDAP Views""" +# """passbook LDAP Views""" # from django.contrib import messages @@ -8,7 +8,7 @@ # from django.urls import reverse # from django.utils.translation import ugettext as _ -# from supervisr.mod.auth.ldap.forms import (AuthenticationBackendSettings, +# from passbook.ldap.forms import (AuthenticationBackendSettings, # ConnectionSettings, # CreateUsersSettings, # GeneralSettingsForm) @@ -34,5 +34,5 @@ # if form.is_valid(): # update_count += form.save() # messages.success(request, _('Successfully updated %d settings.' % update_count)) -# return redirect(reverse('supervisr_mod_auth_ldap:admin_settings')) +# return redirect(reverse('passbook_ldap:admin_settings')) # return render(request, 'ldap/settings.html', render_data) diff --git a/passbook/lib/utils/inline.py b/passbook/lib/utils/inline.py new file mode 100644 index 000000000..4a0c8bae9 --- /dev/null +++ b/passbook/lib/utils/inline.py @@ -0,0 +1,20 @@ +"""passbook core inlining template tags""" +import os + +from django import template +from django.conf import settings + +register = template.Library() + + +@register.simple_tag() +def inline_static(path): + """Inline static asset. If file is binary, return b64 representation""" + prefix = 'data:image/svg+xml;utf8,' + data = '' + full_path = settings.STATIC_ROOT + '/' + path + if os.path.exists(full_path): + if full_path.endswith('.svg'): + with open(full_path) as _file: + data = _file.read() + return prefix + data