stages/email: add form for sending email to prevent spam
stages/email: make token validity configurable
This commit is contained in:
parent
206cf4967d
commit
d4f149bc02
|
@ -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]
|
||||
|
|
|
@ -22,8 +22,6 @@ class EmailStageSerializer(ModelSerializer):
|
|||
"use_ssl",
|
||||
"timeout",
|
||||
"from_address",
|
||||
"ssl_keyfile",
|
||||
"ssl_certfile",
|
||||
]
|
||||
extra_kwargs = {"password": {"write_only": True}}
|
||||
|
||||
|
|
|
@ -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)"),
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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.()
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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")
|
||||
|
|
Reference in New Issue