stages/email: add form for sending email to prevent spam

stages/email: make token validity configurable
This commit is contained in:
Jens Langhammer 2020-05-10 20:50:27 +02:00
parent 206cf4967d
commit d4f149bc02
8 changed files with 65 additions and 30 deletions

View file

@ -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]

View file

@ -22,8 +22,6 @@ class EmailStageSerializer(ModelSerializer):
"use_ssl",
"timeout",
"from_address",
"ssl_keyfile",
"ssl_certfile",
]
extra_kwargs = {"password": {"write_only": True}}

View file

@ -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)"),
}

View file

@ -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."
),
),
]

View file

@ -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):

View file

@ -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.()

View file

@ -1 +1,21 @@
check your emails mate
{% extends 'login/base.html' %}
{% load static %}
{% load i18n %}
{% block card %}
<form method="POST" class="pf-c-form">
<p>
{% blocktrans %}
Check your E-Mails for a password reset link.
{% endblocktrans %}
</p>
{% csrf_token %}
{% block beneath_form %}
{% endblock %}
<div class="pf-c-form__group pf-m-action">
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans "Send Recovery E-Mail." %}</button>
</div>
</form>
{% endblock %}

View file

@ -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")