From d4f149bc02859cd2c3f6ceb557f5b7b9ffb2d3f4 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 10 May 2020 20:50:27 +0200 Subject: [PATCH] stages/email: add form for sending email to prevent spam stages/email: make token validity configurable --- passbook/flows/stage.py | 2 +- passbook/stages/email/api.py | 2 -- passbook/stages/email/forms.py | 12 ++++----- .../migrations/0002_auto_20200510_1844.py | 22 ++++++++++++++++ passbook/stages/email/models.py | 7 +++--- passbook/stages/email/stage.py | 25 +++++++++---------- .../stages/email/waiting_message.html | 22 +++++++++++++++- passbook/stages/email/utils.py | 3 --- 8 files changed, 65 insertions(+), 30 deletions(-) create mode 100644 passbook/stages/email/migrations/0002_auto_20200510_1844.py diff --git a/passbook/flows/stage.py b/passbook/flows/stage.py index 804f9b325..17bc90adb 100644 --- a/passbook/flows/stage.py +++ b/passbook/flows/stage.py @@ -26,7 +26,7 @@ class AuthenticationStage(TemplateView): def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]: kwargs["config"] = CONFIG.y("passbook") - kwargs["title"] = _("Log in to your account") + kwargs["title"] = self.executor.flow.name kwargs["primary_action"] = _("Log in") if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context: kwargs["user"] = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] diff --git a/passbook/stages/email/api.py b/passbook/stages/email/api.py index 14e6c9a3c..063a1020e 100644 --- a/passbook/stages/email/api.py +++ b/passbook/stages/email/api.py @@ -22,8 +22,6 @@ class EmailStageSerializer(ModelSerializer): "use_ssl", "timeout", "from_address", - "ssl_keyfile", - "ssl_certfile", ] extra_kwargs = {"password": {"write_only": True}} diff --git a/passbook/stages/email/forms.py b/passbook/stages/email/forms.py index ae93d428e..1ff829215 100644 --- a/passbook/stages/email/forms.py +++ b/passbook/stages/email/forms.py @@ -5,6 +5,12 @@ from django.utils.translation import gettext_lazy as _ from passbook.stages.email.models import EmailStage +class EmailStageSendForm(forms.Form): + """Form used when sending the e-mail to prevent multiple emails being sent""" + + invalid = forms.CharField(widget=forms.HiddenInput, required=True) + + class EmailStageForm(forms.ModelForm): """Form to create/edit Dummy Stage""" @@ -21,20 +27,14 @@ class EmailStageForm(forms.ModelForm): "use_ssl", "timeout", "from_address", - "ssl_keyfile", - "ssl_certfile", ] widgets = { "name": forms.TextInput(), "host": forms.TextInput(), "username": forms.TextInput(), "password": forms.TextInput(), - "ssl_keyfile": forms.TextInput(), - "ssl_certfile": forms.TextInput(), } labels = { "use_tls": _("Use TLS"), "use_ssl": _("Use SSL"), - "ssl_keyfile": _("SSL Keyfile (optional)"), - "ssl_certfile": _("SSL Certfile (optional)"), } diff --git a/passbook/stages/email/migrations/0002_auto_20200510_1844.py b/passbook/stages/email/migrations/0002_auto_20200510_1844.py new file mode 100644 index 000000000..c7126df7d --- /dev/null +++ b/passbook/stages/email/migrations/0002_auto_20200510_1844.py @@ -0,0 +1,22 @@ +# Generated by Django 3.0.5 on 2020-05-10 18:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_stages_email", "0001_initial"), + ] + + operations = [ + migrations.RemoveField(model_name="emailstage", name="ssl_certfile",), + migrations.RemoveField(model_name="emailstage", name="ssl_keyfile",), + migrations.AddField( + model_name="emailstage", + name="token_expiry", + field=models.IntegerField( + default=30, help_text="Time in minutes the token sent is valid." + ), + ), + ] diff --git a/passbook/stages/email/models.py b/passbook/stages/email/models.py index 57a104ed8..914c118ed 100644 --- a/passbook/stages/email/models.py +++ b/passbook/stages/email/models.py @@ -17,8 +17,9 @@ class EmailStage(Stage): use_ssl = models.BooleanField(default=False) timeout = models.IntegerField(default=10) - ssl_keyfile = models.TextField(default=None, blank=True, null=True) - ssl_certfile = models.TextField(default=None, blank=True, null=True) + token_expiry = models.IntegerField( + default=30, help_text=_("Time in minutes the token sent is valid.") + ) from_address = models.EmailField(default="system@passbook.local") @@ -36,8 +37,6 @@ class EmailStage(Stage): use_tls=self.use_tls, use_ssl=self.use_ssl, timeout=self.timeout, - ssl_certfile=self.ssl_certfile, - ssl_keyfile=self.ssl_keyfile, ) def __str__(self): diff --git a/passbook/stages/email/stage.py b/passbook/stages/email/stage.py index 10c2a0891..c11ead4c3 100644 --- a/passbook/stages/email/stage.py +++ b/passbook/stages/email/stage.py @@ -1,26 +1,28 @@ """passbook multi-stage authentication engine""" from datetime import timedelta -from django.contrib import messages -from django.http import HttpRequest +from django.http import HttpResponse from django.shortcuts import reverse from django.utils.http import urlencode from django.utils.timezone import now from django.utils.translation import gettext as _ +from django.views.generic import FormView from structlog import get_logger from passbook.core.models import Nonce from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER from passbook.flows.stage import AuthenticationStage +from passbook.stages.email.forms import EmailStageSendForm from passbook.stages.email.tasks import send_mails from passbook.stages.email.utils import TemplateEmailMessage LOGGER = get_logger() -class EmailStageView(AuthenticationStage): +class EmailStageView(FormView, AuthenticationStage): """E-Mail stage which sends E-Mail for verification""" + form_class = EmailStageSendForm template_name = "stages/email/waiting_message.html" def get_full_url(self, **kwargs) -> str: @@ -32,13 +34,11 @@ class EmailStageView(AuthenticationStage): relative_url = f"{base_url}?{urlencode(kwargs)}" return self.request.build_absolute_uri(relative_url) - def get(self, request, *args, **kwargs): - # TODO: Form to make sure email is only sent once + def form_invalid(self, form: EmailStageSendForm) -> HttpResponse: pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] - # TODO: Get expiry from Stage setting valid_delta = timedelta( - minutes=31 - ) # 31 because django timesince always rounds down + minutes=self.executor.current_stage.token_expiry + 1 + ) # + 1 because django timesince always rounds down nonce = Nonce.objects.create(user=pending_user, expires=now() + valid_delta) # Send mail to user message = TemplateEmailMessage( @@ -52,12 +52,11 @@ class EmailStageView(AuthenticationStage): }, ) send_mails(self.executor.current_stage, message) - messages.success(request, _("Check your E-Mails for a password reset link.")) # We can't call stage_ok yet, as we're still waiting # for the user to click the link in the email # return self.executor.stage_ok() - return super().get(request, *args, **kwargs) + return super().form_invalid(form) - def post(self, request: HttpRequest): - """Just redirect to next stage""" - return self.executor.stage_ok() + # def post(self, request: HttpRequest): + # """Just redirect to next stage""" + # return self.executor.() diff --git a/passbook/stages/email/templates/stages/email/waiting_message.html b/passbook/stages/email/templates/stages/email/waiting_message.html index 4a4ad8b1d..bebfb7317 100644 --- a/passbook/stages/email/templates/stages/email/waiting_message.html +++ b/passbook/stages/email/templates/stages/email/waiting_message.html @@ -1 +1,21 @@ -check your emails mate +{% extends 'login/base.html' %} + +{% load static %} +{% load i18n %} + +{% block card %} +
+

+ {% blocktrans %} + Check your E-Mails for a password reset link. + {% endblocktrans %} +

+ {% csrf_token %} + + {% block beneath_form %} + {% endblock %} +
+ +
+
+{% endblock %} diff --git a/passbook/stages/email/utils.py b/passbook/stages/email/utils.py index b26b9eab6..139b6af82 100644 --- a/passbook/stages/email/utils.py +++ b/passbook/stages/email/utils.py @@ -1,7 +1,6 @@ """email utils""" from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string -from django.utils.html import strip_tags class TemplateEmailMessage(EmailMultiAlternatives): @@ -9,8 +8,6 @@ class TemplateEmailMessage(EmailMultiAlternatives): def __init__(self, template_name=None, template_context=None, **kwargs): html_content = render_to_string(template_name, template_context) - if "body" not in kwargs: - kwargs["body"] = strip_tags(html_content) super().__init__(**kwargs) self.content_subtype = "html" self.attach_alternative(html_content, "text/html")