Validation of user email addresses

This commit is contained in:
Mira Weller
2025-09-30 15:15:35 +02:00
parent aa8b699b89
commit 22e7962a29
5 changed files with 63 additions and 1 deletions
@@ -0,0 +1,18 @@
# Generated by Django 4.2.23 on 2025-09-04 12:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0288_invoice_transmission"),
]
operations = [
migrations.AddField(
model_name="user",
name="verified_email",
field=models.EmailField(max_length=190, null=True),
),
]
+7
View File
@@ -44,6 +44,7 @@ from django.contrib.auth.models import (
)
from django.contrib.auth.tokens import default_token_generator
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.db import IntegrityError, models, transaction
from django.db.models import Q
from django.utils.crypto import get_random_string, salted_hmac
@@ -242,6 +243,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
verbose_name=_('Email'), max_length=190)
verified_email = models.EmailField(null=True, blank=True, verbose_name=_('Verified Email'), max_length=190)
fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name'))
is_active = models.BooleanField(default=True,
@@ -593,6 +595,11 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
self.session_token = generate_session_token()
self.save(update_fields=['session_token'])
def generate_email_verification_token(self):
token = str(self.pk) + '-' + get_random_string(length=32)
cache.set("confirm_email_token:" + str(self.pk), token, 3 * 86400)
return token
class UserKnownLoginSource(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE, related_name="known_login_sources")
@@ -3,6 +3,11 @@
{% load bootstrap3 %}
{% block title %}{% trans "Account settings" %}{% endblock %}
{% block content %}
{% if user.email != user.verified_email %}
<div class="alert alert-info">
{% trans "Please confirm your email address by clicking on the confirmation link in the email we sent you." %}
</div>
{% endif %}
<h1>{% trans "Account settings" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
+1
View File
@@ -110,6 +110,7 @@ urlpatterns = [
name='user.settings.2fa.confirm.webauthn'),
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
name='user.settings.2fa.delete'),
re_path(r'^confirm/(?P<token>[a-zA-Z0-9-]+)$', user.ConfirmEmailView.as_view(), name='user.email.confirm'),
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/select2$', typeahead.organizer_select2, name='organizers.select2'),
+32 -1
View File
@@ -37,6 +37,7 @@ import json
import logging
import time
from collections import defaultdict
from hmac import compare_digest
from urllib.parse import quote
import webauthn
@@ -59,6 +60,7 @@ from django_otp.plugins.otp_totp.models import TOTPDevice
from django_scopes import scopes_disabled
from webauthn.helpers import generate_challenge, generate_user_handle
from django.core.cache import cache
from pretix.base.auth import get_auth_backends
from pretix.base.forms.auth import ReauthForm
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
@@ -74,6 +76,7 @@ from pretix.control.permissions import (
from pretix.control.views.auth import get_u2f_appid, get_webauthn_rp_id
from pretix.helpers.http import redirect_to_url
from pretix.helpers.u2f import websafe_encode
from pretix.multidomain.urlreverse import build_absolute_uri
REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice)
logger = logging.getLogger(__name__)
@@ -253,9 +256,12 @@ class UserSettings(UpdateView):
msgs.append(_('Your email address has been changed to {email}.').format(email=form.cleaned_data['email']))
if msgs:
self.request.user.send_security_notice(msgs, email=form.cleaned_data['email'])
if self._old_email != form.cleaned_data['email']:
self.request.user.send_security_notice(msgs, email=self._old_email)
token = self.request.user.generate_email_verification_token()
link = build_absolute_uri(False, 'control:user.email.confirm', kwargs={'token': token})
msgs.append(_('Please click the following link to confirm your new email address: {link}').format(link=link))
self.request.user.send_security_notice(msgs, email=form.cleaned_data['email'])
sup = super().form_valid(form)
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data=data)
@@ -834,3 +840,28 @@ class EditStaffSession(StaffMemberRequiredMixin, UpdateView):
return get_object_or_404(StaffSession, pk=self.kwargs['id'])
else:
return get_object_or_404(StaffSession, pk=self.kwargs['id'], user=self.request.user)
class ConfirmEmailView(View):
def get(self, request, token, *args, **kwargs):
try:
uid = int(token.split("-")[0])
except ValueError:
uid = None
if uid and compare_digest(cache.get('confirm_email_token:' + str(uid)), token):
user = User.objects.get(pk=uid)
with transaction.atomic():
if user.email != user.verified_email:
user.log_action("user.email.confirmed", data={
"old_email": user.verified_email,
"new_email": user.email,
})
user.verified_email = user.email
user.save()
messages.success(request, _('Your email has been confirmed.'))
else:
messages.success(request, _('Your email was already confirmed.'))
else:
messages.error(request, _('Invalid confirmation link. Please try again.'))
return redirect("control:user.settings")