Compare commits

...

2 Commits

Author SHA1 Message Date
Raphael Michel
225d47ef15 Apply suggestions from code review 2026-01-26 11:26:03 +01:00
Raphael Michel
b4608da1b4 Customer accounts: Add security notices 2025-12-09 12:16:13 +01:00
6 changed files with 75 additions and 0 deletions

View File

@@ -40,6 +40,7 @@ from i18nfield.fields import I18nCharField
from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.banlist import banned
from pretix.base.i18n import language
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.giftcards import GiftCardTransaction
@@ -164,6 +165,28 @@ class Customer(LoggedModel):
self.attendee_profiles.all().delete()
self.invoice_addresses.all().delete()
def send_security_notice(self, message, email=None):
from pretix.base.services.mail import SendMailException, mail
from pretix.multidomain.urlreverse import build_absolute_uri
try:
with language(self.locale):
mail(
email or self.email,
self.organizer.settings.mail_subject_customer_security_notice,
self.organizer.settings.mail_text_customer_security_notice,
{
**self.get_email_context(),
'message': str(message),
'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index')
},
customer=self,
organizer=self.organizer,
locale=self.locale
)
except SendMailException:
pass # Already logged
@scopes_disabled()
def assign_identifier(self):
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ23456789')

View File

@@ -2925,6 +2925,28 @@ If you did not request a new password, please ignore this email.
Best regards,
Your {organizer} team""")) # noqa: W291
},
'mail_subject_customer_security_notice': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Changes to your account at {organizer}")),
},
'mail_text_customer_security_notice': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {name},
the following change has been made to your account at {organizer}:
{message}
You can review and change your account settings here:
{url}
If this change was not performed by you, please contact us immediately.
Best regards,
Your {organizer} team""")) # noqa: W291
},
'smtp_use_custom': {

View File

@@ -633,6 +633,16 @@ class MailSettingsForm(SettingsForm):
required=False,
widget=I18nMarkdownTextarea,
)
mail_subject_customer_security_notice = I18nFormField(
label=_("Subject"),
required=False,
widget=I18nTextInput,
)
mail_text_customer_security_notice = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
)
base_context = {
'mail_text_customer_registration': ['customer', 'url'],
@@ -641,6 +651,8 @@ class MailSettingsForm(SettingsForm):
'mail_subject_customer_email_change': ['customer', 'url'],
'mail_text_customer_reset': ['customer', 'url'],
'mail_subject_customer_reset': ['customer', 'url'],
'mail_text_customer_security_notice': ['customer', 'url', 'message'],
'mail_subject_customer_security_notice': ['customer', 'url', 'message'],
}
def _get_sample_context(self, base_parameters):
@@ -654,6 +666,9 @@ class MailSettingsForm(SettingsForm):
'presale:organizer.customer.activate'
) + '?token=' + get_random_string(30)
if 'message' in base_parameters:
placeholders['message'] = _('Your password has been changed.')
if 'customer' in base_parameters:
placeholders['name'] = pgettext_lazy('person_name_sample', 'John Doe')
name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme]

View File

@@ -65,6 +65,9 @@
{% blocktrans asvar title_reset %}Customer account password reset{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="reset" title=title_reset items="mail_subject_customer_reset,mail_text_customer_reset" %}
{% blocktrans asvar title_security_notice %}Customer account security notification{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="security_notice" title=title_security_notice items="mail_subject_customer_security_notice,mail_text_customer_security_notice" %}
</div>
</fieldset>
</div>

View File

@@ -286,6 +286,7 @@ class SetPasswordView(FormView):
self.customer.is_verified = True
self.customer.save()
self.customer.log_action('pretix.customer.password.set', {})
self.customer.send_security_notice(_("Your password has been changed."))
messages.success(
self.request,
_('Your new password has been set! You can now use it to log in.'),
@@ -540,6 +541,7 @@ class ChangePasswordView(CustomerAccountBaseMixin, FormView):
customer.set_password(form.cleaned_data['password'])
customer.save()
messages.success(self.request, _('Your changes have been saved.'))
customer.send_security_notice(_("Your password has been changed."))
update_customer_session_auth_hash(self.request, customer)
return HttpResponseRedirect(self.get_success_url())
@@ -630,11 +632,15 @@ class ConfirmChangeView(View):
try:
with transaction.atomic():
old_email = customer.email
customer.email = data['email']
customer.save()
customer.log_action('pretix.customer.changed', {
'email': data['email']
})
msg = _('Your email address has been changed from {old_email} to {email}.').format(old_email=old_email, email=customer.email)
customer.send_security_notice(msg, email=old_email)
customer.send_security_notice(msg, email=customer.email)
except IntegrityError:
messages.success(request, _('Your email address has not been updated since the address is already in use '
'for another customer account.'))

View File

@@ -164,6 +164,7 @@ def test_org_resetpw(env, client):
customer.refresh_from_db()
assert customer.check_password('PANioMR62')
assert customer.is_verified
assert len(djmail.outbox) == 2
@pytest.mark.django_db
@@ -623,6 +624,7 @@ def test_change_email(env, client):
customer.refresh_from_db()
assert customer.email == 'john@example.org'
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == ['john@example.com']
token = dumps({
'customer': customer.pk,
@@ -632,6 +634,9 @@ def test_change_email(env, client):
assert r.status_code == 302
customer.refresh_from_db()
assert customer.email == 'john@example.com'
assert len(djmail.outbox) == 3
assert djmail.outbox[1].to == ['john@example.org']
assert djmail.outbox[2].to == ['john@example.com']
@pytest.mark.django_db
@@ -673,6 +678,7 @@ def test_change_pw(env, client, client2):
r = client.get('/bigevents/account/password')
assert r.status_code == 200
assert len(djmail.outbox) == 1
# Client 2 got logged out
r = client2.post('/bigevents/account/password')