Add email verification flow for existing users

This commit is contained in:
Mira Weller
2025-10-30 13:45:09 +01:00
parent eb731f305b
commit 35ac277b3d
4 changed files with 66 additions and 21 deletions

View File

@@ -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( 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, 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: else:
raise Exception('Invalid confirmation code reason') raise Exception('Invalid confirmation code reason')

View File

@@ -5,7 +5,20 @@
{% block content %} {% block content %}
{% if not user.is_verified %} {% if not user.is_verified %}
<div class="alert alert-info"> <div class="alert alert-info">
{% trans "Please confirm your email address by clicking on the confirmation link in the email we sent you." %} <p>
{% 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 %}
</p>
<p>
<form action="{% url "control:user.settings.email.send_verification_code" %}" method="post" class="form-horizontal">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
{% trans "Send confirmation email" %}
</button>
</form>
</p>
</div> </div>
{% endif %} {% endif %}
<h1>{% trans "Account settings" %}</h1> <h1>{% trans "Account settings" %}</h1>
@@ -73,7 +86,7 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-3 control-label" for="">{% trans "Authorized applications" %}</label> <label class="col-md-3 control-label">{% trans "Authorized applications" %}</label>
<div class="col-md-9 static-form-row"> <div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.oauth.list" %}"> <a href="{% url "control:user.settings.oauth.list" %}">
<span class="fa fa-plug"></span> <span class="fa fa-plug"></span>
@@ -82,7 +95,7 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-3 control-label" for="">{% trans "Account history" %}</label> <label class="col-md-3 control-label">{% trans "Account history" %}</label>
<div class="col-md-9 static-form-row"> <div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.history" %}"> <a href="{% url "control:user.settings.history" %}">
<span class="fa fa-history"></span> <span class="fa fa-history"></span>

View File

@@ -112,6 +112,7 @@ urlpatterns = [
name='user.settings.2fa.delete'), 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/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/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'^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/$', organizer.OrganizerList.as_view(), name='organizers'),
re_path(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'), re_path(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),

View File

@@ -50,6 +50,7 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property 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.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -890,13 +891,30 @@ class UserEmailChangeView(RecentAuthenticationRequiredMixin, FormView):
email=form.cleaned_data['new_email'], email=form.cleaned_data['new_email'],
state=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): def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.')) messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form) 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): class UserEmailConfirmView(FormView):
form_class = ConfirmationCodeForm form_class = ConfirmationCodeForm
template_name = 'pretixcontrol/user/confirmation_code_dialog.html' template_name = 'pretixcontrol/user/confirmation_code_dialog.html'
@@ -905,43 +923,52 @@ class UserEmailConfirmView(FormView):
return { return {
**super().get_context_data(**kwargs), **super().get_context_data(**kwargs),
"cancel_url": reverse('control:user.settings', 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 <strong>{email}</strong>."),
email=self.request.session.get('email_confirmation_destination', ''),
),
} }
@transaction.atomic() @transaction.atomic()
def form_valid(self, form): def form_valid(self, form):
reason = self.request.GET['reason']
if reason not in ('email_change', 'email_verify'):
raise PermissionDenied
try: try:
new_email = self.request.user.check_confirmation_code( new_email = self.request.user.check_confirmation_code(
session=self.request.session, session=self.request.session,
reason='email_change', reason=reason,
code=form.cleaned_data['code'], code=form.cleaned_data['code'],
) )
except PermissionDenied: except PermissionDenied:
return self.form_invalid(form) return self.form_invalid(form)
except BadRequest: except BadRequest:
messages.error(self.request, _( messages.error(self.request, _(
'Your email address could not be changed, because we were unable to ' 'We were unable to verify your confirmation code. Please try again.'
'verify your confirmation code. Please try again.'
)) ))
return redirect(reverse('control:user.settings', kwargs={})) return redirect(reverse('control:user.settings', kwargs={}))
msgs = [] log_data = {
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,
'email': new_email, 'email': new_email,
'email_verified': True, '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) 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={})) return redirect(reverse('control:user.settings', kwargs={}))
def form_invalid(self, form): def form_invalid(self, form):