From 35ac277b3ddd5ea1dddfef4cfd1e4769d7fbce77 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Thu, 30 Oct 2025 13:45:09 +0100 Subject: [PATCH] Add email verification flow for existing users --- src/pretix/base/models/auth.py | 4 ++ .../pretixcontrol/user/settings.html | 19 +++++- src/pretix/control/urls.py | 1 + src/pretix/control/views/user.py | 63 +++++++++++++------ 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index 0b9eb8da1e..d1c98a1a73 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -380,6 +380,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): msg = str(_('to confirm changing your email address from {old_email}\nto {new_email}, use the following code:').format( old_email=self.email, new_email=email, )) + elif reason == 'email_verify': + msg = str(_('to confirm that your email address {email} belongs to your pretix account, use the following code:').format( + email=self.email, + )) else: raise Exception('Invalid confirmation code reason') diff --git a/src/pretix/control/templates/pretixcontrol/user/settings.html b/src/pretix/control/templates/pretixcontrol/user/settings.html index 1641c505d6..b4cb429bfa 100644 --- a/src/pretix/control/templates/pretixcontrol/user/settings.html +++ b/src/pretix/control/templates/pretixcontrol/user/settings.html @@ -5,7 +5,20 @@ {% block content %} {% if not user.is_verified %}
- {% trans "Please confirm your email address by clicking on the confirmation link in the email we sent you." %} +

+ {% blocktrans trimmed %} + Your email address is not confirmed yet. To secure your account, please confirm your email address by + clicking a confirmation link we send to your email address. + {% endblocktrans %} +

+

+

+ {% csrf_token %} + +
+

{% endif %}

{% trans "Account settings" %}

@@ -73,7 +86,7 @@
- +
- +
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index ed225f42d4..ee34e3b768 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -112,6 +112,7 @@ urlpatterns = [ name='user.settings.2fa.delete'), re_path(r'^settings/email/confirm$', user.UserEmailConfirmView.as_view(), name='user.settings.email.confirm'), re_path(r'^settings/email/change$', user.UserEmailChangeView.as_view(), name='user.settings.email.change'), + re_path(r'^settings/email/verify', user.UserEmailVerifyView.as_view(), name='user.settings.email.send_verification_code'), re_path(r'^settings/password/change$', user.UserPasswordChangeView.as_view(), name='user.settings.password.change'), re_path(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'), re_path(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'), diff --git a/src/pretix/control/views/user.py b/src/pretix/control/views/user.py index fedc425a94..d78d5959fb 100644 --- a/src/pretix/control/views/user.py +++ b/src/pretix/control/views/user.py @@ -50,6 +50,7 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils.crypto import get_random_string from django.utils.functional import cached_property +from django.utils.html import format_html from django.utils.http import url_has_allowed_host_and_scheme from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ @@ -890,13 +891,30 @@ class UserEmailChangeView(RecentAuthenticationRequiredMixin, FormView): email=form.cleaned_data['new_email'], state=form.cleaned_data['new_email'], ) - return redirect(reverse('control:user.settings.email.confirm', kwargs={})) + self.request.session['email_confirmation_destination'] = form.cleaned_data['new_email'] + return redirect(reverse('control:user.settings.email.confirm', kwargs={}) + '?reason=email_change') def form_invalid(self, form): messages.error(self.request, _('We could not save your changes. See below for details.')) return super().form_invalid(form) +class UserEmailVerifyView(View): + def post(self, request, *args, **kwargs): + if self.request.user.is_verified: + messages.success(self.request, _('Your email address was already verified.')) + return redirect(reverse('control:user.settings', kwargs={})) + + self.request.user.send_confirmation_code( + session=self.request.session, + reason='email_verify', + email=self.request.user.email, + state=self.request.user.email, + ) + self.request.session['email_confirmation_destination'] = self.request.user.email + return redirect(reverse('control:user.settings.email.confirm', kwargs={}) + '?reason=email_verify') + + class UserEmailConfirmView(FormView): form_class = ConfirmationCodeForm template_name = 'pretixcontrol/user/confirmation_code_dialog.html' @@ -905,43 +923,52 @@ class UserEmailConfirmView(FormView): return { **super().get_context_data(**kwargs), "cancel_url": reverse('control:user.settings', kwargs={}), - "message": _("Please enter the confirmation code we sent to your new email address."), + "message": format_html( + _("Please enter the confirmation code we sent to your email address {email}."), + email=self.request.session.get('email_confirmation_destination', ''), + ), } @transaction.atomic() def form_valid(self, form): + reason = self.request.GET['reason'] + if reason not in ('email_change', 'email_verify'): + raise PermissionDenied try: new_email = self.request.user.check_confirmation_code( session=self.request.session, - reason='email_change', + reason=reason, code=form.cleaned_data['code'], ) except PermissionDenied: return self.form_invalid(form) except BadRequest: messages.error(self.request, _( - 'Your email address could not be changed, because we were unable to ' - 'verify your confirmation code. Please try again.' + 'We were unable to verify your confirmation code. Please try again.' )) return redirect(reverse('control:user.settings', kwargs={})) - msgs = [] - msgs.append(_('Your email address has been changed to {email}.').format(email=new_email)) - old_email = self.request.user.email - self.request.user.send_security_notice(msgs, email=old_email) - self.request.user.send_security_notice(msgs, email=new_email) - - self.request.user.email = new_email - self.request.user.is_verified = True - self.request.user.save() - self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data={ - 'old_email': old_email, + log_data = { 'email': new_email, 'email_verified': True, - }) + } + if reason == 'email_change': + msgs = [] + msgs.append(_('Your email address has been changed to {email}.').format(email=new_email)) + log_data['old_email'] = old_email = self.request.user.email + self.request.user.send_security_notice(msgs, email=old_email) + self.request.user.send_security_notice(msgs, email=new_email) + + self.request.user.email = new_email + self.request.user.is_verified = True + self.request.user.save() + self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data=log_data) update_session_auth_hash(self.request, self.request.user) - messages.success(self.request, _('Your email address has been changed successfully.')) + if reason == 'email_change': + messages.success(self.request, _('Your email address has been changed successfully.')) + else: + messages.success(self.request, _('Your email address has been confirmed successfully.')) return redirect(reverse('control:user.settings', kwargs={})) def form_invalid(self, form):