Compare commits

..

7 Commits

Author SHA1 Message Date
Raphael Michel
c229e5ad5a Mail: Fix stuck state when tickets are not available (Z#23225229) 2026-02-22 15:02:38 +01:00
Raphael Michel
769e1312d4 Revert "Disable partitioned cookies for Safari due to WebKit bugs (#5843)"
This reverts commit fbd8bbbeaa.
2026-02-20 10:08:51 +01:00
Martin Gross
3d53c03906 Stripe: isort 2026-02-19 14:43:27 +01:00
Martin Gross
59d1d2cb16 Stripe: Add Wero as a hidden payment method (private beta; requires MoR) 2026-02-19 14:40:01 +01:00
luelista
7e45837295 Security hardening for 2FA configuration (#5685)
* reduce default RecentAuthenticationRequiredMixin timeout to 15 min
* never cache pages with RecentAuthenticationRequiredMixin
* show emergency codes only once after generating
2026-02-19 12:43:23 +01:00
Lukas Bockstaller
fd9ed15065 include acceptor slug in log/webhook event (#5906) 2026-02-19 10:00:11 +01:00
Richard Schreiber
2df3d9206b Add voucher tag to orderlist positions export 2026-02-19 09:42:00 +01:00
8 changed files with 73 additions and 50 deletions

View File

@@ -651,6 +651,7 @@ class OrderListExporter(MultiSheetListExporter):
pgettext('address', 'State'),
_('Voucher'),
_('Voucher budget usage'),
_('Voucher tag'),
_('Pseudonymization ID'),
_('Ticket secret'),
_('Seat ID'),
@@ -769,6 +770,7 @@ class OrderListExporter(MultiSheetListExporter):
op.state_for_address or '',
op.voucher.code if op.voucher else '',
op.voucher_budget_use if op.voucher_budget_use else '',
op.voucher.tag if op.voucher else '',
op.pseudonymization_id,
op.secret,
]

View File

@@ -389,7 +389,7 @@ def mail_send_task(self, **kwargs) -> bool:
# mail_send_task(self, *, outgoing_mail)
with scopes_disabled():
mail_send(**kwargs)
return
return False
else:
raise ValueError("Unknown arguments")
@@ -443,15 +443,24 @@ def mail_send_task(self, **kwargs) -> bool:
content = ct.file.read()
args.append((name, content, ct.type))
attach_size += len(content)
except Exception:
except Exception as e:
# This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out
# why (probably some race condition with ticket cache invalidation?), so retry later.
try:
self.retry(max_retries=5, countdown=60)
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}, will retry')
retry_after = 60
outgoing_mail.error = "Tickets not ready"
outgoing_mail.error_detail = str(e)
outgoing_mail.sent = now()
outgoing_mail.status = OutgoingMail.STATUS_AWAITING_RETRY
outgoing_mail.retry_after = now() + timedelta(seconds=retry_after)
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after",
"actual_attachments"])
self.retry(max_retries=5, countdown=retry_after)
except MaxRetriesExceededError:
# Well then, something is really wrong, let's send it without attachment before we
# don't send at all
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}')
logger.exception(f'Too many retries attaching tickets to email {outgoing_mail.guid}, skip attachment')
pass
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:

View File

@@ -144,14 +144,23 @@
</div>
<div class="panel-body">
<p>
{% trans "If you lose access to your devices, you can use one of the following keys to log in. We recommend to store them in a safe place, e.g. printed out or in a password manager. Every token can be used at most once." %}
{% blocktrans trimmed %}
If you lose access to your devices, you can use one of your emergency tokens to log in.
We recommend to store them in a safe place, e.g. printed out or in a password manager.
Every token can be used at most once.
{% endblocktrans %}
</p>
<p>{% trans "Unused tokens:" %}</p>
<ul>
{% for t in static_tokens %}
<li><code>{{ t.token }}</code></li>
{% endfor %}
</ul>
{% if static_tokens_device %}
<p>
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
You generated your emergency tokens on {{ generation_date_time }}.
{% endblocktrans %}
</p>
{% else %}
<p>
{% trans "You don't have any emergency tokens yet." %}
</p>
{% endif %}
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
<span class="fa fa-refresh"></span>
{% trans "Generate new emergency tokens" %}

View File

@@ -49,12 +49,14 @@ from django.db import transaction
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.decorators import method_decorator
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 _
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.generic import FormView, ListView, TemplateView, UpdateView
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
@@ -85,8 +87,9 @@ logger = logging.getLogger(__name__)
class RecentAuthenticationRequiredMixin:
max_time = 3600
max_time = 900
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
if tdelta > self.max_time:
@@ -289,16 +292,13 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
ctx = super().get_context_data()
try:
ctx['static_tokens'] = StaticDevice.objects.get(user=self.request.user, name='emergency').token_set.all()
ctx['static_tokens_device'] = StaticDevice.objects.get(user=self.request.user, name='emergency')
except StaticDevice.MultipleObjectsReturned:
ctx['static_tokens'] = StaticDevice.objects.filter(
ctx['static_tokens_device'] = StaticDevice.objects.filter(
user=self.request.user, name='emergency'
).first().token_set.all()
).first()
except StaticDevice.DoesNotExist:
d = StaticDevice.objects.create(user=self.request.user, name='emergency')
for i in range(10):
d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
ctx['static_tokens'] = d.token_set.all()
ctx['static_tokens_device'] = None
ctx['devices'] = []
for dt in REAL_DEVICE_TYPES:
@@ -631,7 +631,8 @@ class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, Template
self.request.user.update_session_token()
update_session_auth_hash(self.request, self.request.user)
messages.success(request, _('Your emergency codes have been newly generated. Remember to store them in a safe '
'place in case you lose access to your devices.'))
'place in case you lose access to your devices. You will not be able to view them '
'again here.\n\nYour emergency codes:\n- ' + '\n- '.join(t.token for t in d.token_set.all())))
return redirect(reverse('control:user.settings.2fa'))

View File

@@ -34,10 +34,7 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
if not is_secure:
# https://www.chromestatus.com/feature/5633521622188032
return
useragent = request.headers.get('User-Agent', '')
if should_send_same_site_none(useragent):
if should_send_same_site_none(request.headers.get('User-Agent', '')):
# Chromium is rolling out SameSite=Lax as a default
# https://www.chromestatus.com/feature/5088147346030592
# This however breaks all pretix-in-an-iframe things, such as the pretix Widget.
@@ -47,29 +44,8 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
# This will only work on secure cookies as well
# https://www.chromestatus.com/feature/5633521622188032
response.cookies[key]['secure'] = is_secure
if can_send_partitioned_cookie(useragent):
# CHIPS
response.cookies[key]['Partitioned'] = True
def can_send_partitioned_cookie(useragent):
# Safari currently exhibits a bug where Partitioned cookies (CHIPS) are not
# sent back to the originating site after multi-hop cross-site redirects,
# breaking SSO login flows in pretix.
#
# Partitioned cookies were initially introduced in Safari 18.4, removed
# again in 18.5 due to a bug, and reintroduced in Safari 26.2, where the
# current issue is present.
#
# Once the Safari issue is fixed, this check should be refined to be
# conditional on the affected versions only.
#
# WebKit issues:
#
# - https://bugs.webkit.org/show_bug.cgi?id=292975
# - https://bugs.webkit.org/show_bug.cgi?id=306194
return not is_safari(useragent)
# CHIPS
response.cookies[key]['Partitioned'] = True
# Based on https://www.chromium.org/updates/same-site/incompatible-clients

View File

@@ -118,6 +118,7 @@ logger = logging.getLogger('pretix.plugins.stripe')
# - UPI: ✗
# - Netbanking: ✗
# - TWINT: ✓
# - Wero: ✓ (No settings UI yet)
#
# Bank transfers
# - ACH Bank Transfer: ✗
@@ -509,6 +510,15 @@ class StripeSettingsHolder(BasePaymentProvider):
'before they work properly.'),
required=False,
)),
# Disabled for now, since still in closed Beta and only available to dedicated boarded accounts.
# ('method_wero',
# forms.BooleanField(
# label=_('Wero'),
# disabled=self.event.currency not in 'EUR',
# help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account '
# 'before they work properly.'),
# required=False,
# )),
] + extra_fields + list(super().settings_form_fields.items()) + moto_settings
)
if not self.settings.connect_client_id or self.settings.secret_key:
@@ -1946,3 +1956,15 @@ class StripeMobilePay(StripeRedirectMethod):
"type": "mobilepay",
},
}
class StripeWero(StripeRedirectMethod):
identifier = 'stripe_wero'
verbose_name = _('WERO via Stripe')
public_name = 'WERO'
method = 'wero'
confirmation_method = 'automatic'
explanation = _(
'This payment method is available to European online banking users, whose banking institutions support WERO '
'either through their native banking apps or through the WERO wallet app. Please have you app ready.'
)

View File

@@ -49,14 +49,14 @@ def register_payment_provider(sender, **kwargs):
StripeMultibanco, StripePayByBank, StripePayPal, StripePromptPay,
StripePrzelewy24, StripeRevolutPay, StripeSEPADirectDebit,
StripeSettingsHolder, StripeSofort, StripeSwish, StripeTwint,
StripeWeChatPay,
StripeWeChatPay, StripeWero,
)
return [
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
StripeSofort, StripeEPS, StripeMultibanco, StripePayByBank, StripePrzelewy24, StripePromptPay, StripeRevolutPay,
StripeWeChatPay, StripeSEPADirectDebit, StripeAffirm, StripeKlarna, StripePayPal, StripeSwish,
StripeTwint, StripeMobilePay
StripeTwint, StripeMobilePay, StripeWero
]

View File

@@ -339,13 +339,17 @@ class UserSettings2FATest(SoupTest):
def test_gen_emergency(self):
self.client.get('/control/settings/2fa/')
assert not StaticDevice.objects.filter(user=self.user, name='emergency').exists()
self.client.post('/control/settings/2fa/regenemergency')
d = StaticDevice.objects.get(user=self.user, name='emergency')
assert d.token_set.count() == 10
old_tokens = set(t.token for t in d.token_set.all())
self.client.post('/control/settings/2fa/regenemergency')
new_tokens = set(t.token for t in d.token_set.all())
d = StaticDevice.objects.get(user=self.user, name='emergency')
assert d.token_set.count() == 10
new_tokens = set(t.token for t in d.token_set.all())
assert old_tokens != new_tokens
def test_delete_u2f(self):