stages/email: add form for sending email to prevent spam
stages/email: make token validity configurable
This commit is contained in:
		| @ -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)"), | ||||
|         } | ||||
|  | ||||
							
								
								
									
										22
									
								
								passbook/stages/email/migrations/0002_auto_20200510_1844.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								passbook/stages/email/migrations/0002_auto_20200510_1844.py
									
									
									
									
									
										Normal 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." | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -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
	
	Block a user
	 Jens Langhammer
					Jens Langhammer