forked from CGM_Public/pretix_original
Compare commits
7 Commits
manual-ins
...
bundles-no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9692283df | ||
|
|
61b25acdd2 | ||
|
|
6cc9529d9a | ||
|
|
cdc5401dc2 | ||
|
|
1334a570e4 | ||
|
|
7a66aea2cb | ||
|
|
ee77a5e447 |
@@ -294,6 +294,10 @@ Example::
|
||||
setting is not provided, pretix will generate a random secret on the first start
|
||||
and will store it in the filesystem for later usage.
|
||||
|
||||
``secret_fallback0`` ... ``secret_fallback9``
|
||||
Prior versions of the secret to be used by Django for signing and verification purposes that will still
|
||||
be accepted but no longer be used for new signing.
|
||||
|
||||
``debug``
|
||||
Whether or not to run in debug mode. Default is ``False``.
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ To build and run pretix, you will need the following debian packages::
|
||||
|
||||
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
|
||||
libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||
gettext libpq-dev libjpeg-dev libopenjp2-7-dev 2to3
|
||||
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
|
||||
|
||||
Config file
|
||||
-----------
|
||||
|
||||
@@ -571,13 +571,23 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
|
||||
def get_session_auth_hash(self):
|
||||
"""
|
||||
Return an HMAC that needs to
|
||||
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
|
||||
logout after every password change.
|
||||
"""
|
||||
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
|
||||
|
||||
def get_session_auth_fallback_hash(self):
|
||||
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
|
||||
yield self._get_session_auth_hash(secret=fallback_secret)
|
||||
|
||||
def _get_session_auth_hash(self, secret):
|
||||
"""
|
||||
"""
|
||||
key_salt = "pretix.base.models.User.get_session_auth_hash"
|
||||
payload = self.password
|
||||
payload += self.email
|
||||
payload += self.session_token
|
||||
return salted_hmac(key_salt, payload).hexdigest()
|
||||
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
|
||||
|
||||
def update_session_token(self):
|
||||
self.session_token = generate_session_token()
|
||||
|
||||
@@ -219,13 +219,24 @@ class Customer(LoggedModel):
|
||||
return is_password_usable(self.password)
|
||||
|
||||
def get_session_auth_hash(self):
|
||||
"""
|
||||
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
|
||||
logout after every password change.
|
||||
"""
|
||||
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
|
||||
|
||||
def get_session_auth_fallback_hash(self):
|
||||
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
|
||||
yield self._get_session_auth_hash(secret=fallback_secret)
|
||||
|
||||
def _get_session_auth_hash(self, secret):
|
||||
"""
|
||||
Return an HMAC of the password field.
|
||||
"""
|
||||
key_salt = "pretix.base.models.customers.Customer.get_session_auth_hash"
|
||||
payload = self.password
|
||||
payload += self.email
|
||||
return salted_hmac(key_salt, payload).hexdigest()
|
||||
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
|
||||
|
||||
def get_email_context(self):
|
||||
from pretix.base.settings import get_name_parts_localized
|
||||
|
||||
@@ -40,6 +40,7 @@ import json
|
||||
import logging
|
||||
import operator
|
||||
import string
|
||||
import warnings
|
||||
from collections import Counter
|
||||
from datetime import datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
@@ -381,8 +382,28 @@ class Order(LockModel, LoggedModel):
|
||||
self.event.cache.delete('complain_testmode_orders')
|
||||
self.delete()
|
||||
|
||||
def email_confirm_secret(self):
|
||||
return self.tagged_secret("email_confirm", 9)
|
||||
|
||||
def email_confirm_hash(self):
|
||||
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
|
||||
warnings.warn('Use email_confirm_secret() instead of email_confirm_hash().',
|
||||
DeprecationWarning)
|
||||
return self.email_confirm_secret()
|
||||
|
||||
def check_email_confirm_secret(self, received_secret):
|
||||
return (
|
||||
hmac.compare_digest(
|
||||
self.tagged_secret("email_confirm", 9),
|
||||
received_secret[:9].lower()
|
||||
) or any(
|
||||
# TODO: remove this clause after a while (compatibility with old secrets currently in flight)
|
||||
hmac.compare_digest(
|
||||
hashlib.sha256(sk.encode() + self.secret.encode()).hexdigest()[:9],
|
||||
received_secret
|
||||
)
|
||||
for sk in [settings.SECRET_KEY, *settings.SECRET_KEY_FALLBACKS]
|
||||
)
|
||||
)
|
||||
|
||||
def get_extended_status_display(self):
|
||||
# Changes in this method should to be replicated in pretixcontrol/orders/fragment_order_status.html
|
||||
|
||||
@@ -306,8 +306,11 @@ class TaxRule(LoggedModel):
|
||||
|
||||
if rate == Decimal('0.00'):
|
||||
return TaxedPrice(
|
||||
net=base_price - subtract_from_gross, gross=base_price - subtract_from_gross, tax=Decimal('0.00'),
|
||||
rate=rate, name=self.name
|
||||
net=max(Decimal('0.00'), base_price - subtract_from_gross),
|
||||
gross=max(Decimal('0.00'), base_price - subtract_from_gross),
|
||||
tax=Decimal('0.00'),
|
||||
rate=rate,
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
if base_price_is == 'auto':
|
||||
|
||||
@@ -301,7 +301,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
order.event, 'presale:event.order.open', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_hash()
|
||||
'hash': order.email_confirm_secret()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -262,7 +262,7 @@ def base_placeholders(sender, **kwargs):
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_hash()
|
||||
'hash': order.email_confirm_secret()
|
||||
}
|
||||
), lambda event: build_absolute_uri(
|
||||
event,
|
||||
@@ -443,7 +443,7 @@ def base_placeholders(sender, **kwargs):
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_hash(),
|
||||
'hash': order.email_confirm_secret(),
|
||||
}),
|
||||
)
|
||||
for order in orders
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
<div class="order-button">
|
||||
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}" class="button">
|
||||
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_secret order=order.code secret=order.secret %}" class="button">
|
||||
{% trans "View order details" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -239,11 +239,14 @@ class VoucherForm(I18nModelForm):
|
||||
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
|
||||
)
|
||||
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
|
||||
if 'seat' in self.fields and data.get('seat'):
|
||||
self.instance.seat = Voucher.clean_seat_id(
|
||||
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
|
||||
)
|
||||
self.instance.item = self.instance.seat.product
|
||||
if 'seat' in self.fields:
|
||||
if data.get('seat'):
|
||||
self.instance.seat = Voucher.clean_seat_id(
|
||||
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
|
||||
)
|
||||
self.instance.item = self.instance.seat.product
|
||||
else:
|
||||
self.instance.seat = None
|
||||
|
||||
voucher_form_validation.send(sender=self.instance.event, form=self, data=data)
|
||||
|
||||
|
||||
@@ -100,10 +100,23 @@ def get_customer(request):
|
||||
request._cached_customer = None
|
||||
else:
|
||||
session_hash = session.get(hash_session_key)
|
||||
session_auth_hash = customer.get_session_auth_hash()
|
||||
session_hash_verified = session_hash and constant_time_compare(
|
||||
session_hash,
|
||||
customer.get_session_auth_hash()
|
||||
session_auth_hash,
|
||||
)
|
||||
if not session_hash_verified:
|
||||
# If the current secret does not verify the session, try
|
||||
# with the fallback secrets and stop when a matching one is
|
||||
# found.
|
||||
if session_hash and any(
|
||||
constant_time_compare(session_hash, fallback_auth_hash)
|
||||
for fallback_auth_hash in customer.get_session_auth_fallback_hash()
|
||||
):
|
||||
request.session.cycle_key()
|
||||
request.session[hash_session_key] = session_auth_hash
|
||||
session_hash_verified = True
|
||||
|
||||
if session_hash_verified:
|
||||
request._cached_customer = customer
|
||||
else:
|
||||
|
||||
@@ -156,11 +156,10 @@ class OrderOpen(EventViewMixin, OrderDetailMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
if kwargs.get('hash') == self.order.email_confirm_hash():
|
||||
if not self.order.email_known_to_work:
|
||||
self.order.log_action('pretix.event.order.contact.confirmed')
|
||||
self.order.email_known_to_work = True
|
||||
self.order.save(update_fields=['email_known_to_work'])
|
||||
if self.order.check_email_confirm_secret(kwargs.get('hash')) and not self.order.email_known_to_work:
|
||||
self.order.log_action('pretix.event.order.contact.confirmed')
|
||||
self.order.email_known_to_work = True
|
||||
self.order.save(update_fields=['email_known_to_work'])
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
|
||||
|
||||
@@ -94,6 +94,13 @@ else:
|
||||
pass # os.chown is not available on Windows
|
||||
f.write(SECRET_KEY)
|
||||
|
||||
|
||||
SECRET_KEY_FALLBACKS = []
|
||||
for i in range(10):
|
||||
if config.has_option('django', f'secret_fallback{i}'):
|
||||
SECRET_KEY_FALLBACKS.append(config.get('django', f'secret_fallback{i}'))
|
||||
|
||||
|
||||
# Adjustable settings
|
||||
|
||||
debug_fallback = "runserver" in sys.argv or "runserver_plus" in sys.argv
|
||||
|
||||
54
src/pretix/static/npm_dir/package-lock.json
generated
54
src/pretix/static/npm_dir/package-lock.json
generated
@@ -11,7 +11,7 @@
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-vue": "^5.0.1",
|
||||
"vue": "^2.7.16",
|
||||
@@ -1734,14 +1734,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "15.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
||||
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
|
||||
"version": "15.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz",
|
||||
"integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"@types/resolve": "1.20.2",
|
||||
"deepmerge": "^4.2.2",
|
||||
"is-builtin-module": "^3.2.1",
|
||||
"is-module": "^1.0.0",
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
@@ -2102,17 +2101,6 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
@@ -2779,20 +2767,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-builtin-module": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
|
||||
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
|
||||
"dependencies": {
|
||||
"builtin-modules": "^3.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||
@@ -5162,14 +5136,13 @@
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-node-resolve": {
|
||||
"version": "15.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
||||
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
|
||||
"version": "15.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz",
|
||||
"integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==",
|
||||
"requires": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"@types/resolve": "1.20.2",
|
||||
"deepmerge": "^4.2.2",
|
||||
"is-builtin-module": "^3.2.1",
|
||||
"is-module": "^1.0.0",
|
||||
"resolve": "^1.22.1"
|
||||
}
|
||||
@@ -5417,11 +5390,6 @@
|
||||
"update-browserslist-db": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw=="
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
@@ -5920,14 +5888,6 @@
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-builtin-module": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
|
||||
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
|
||||
"requires": {
|
||||
"builtin-modules": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"is-core-module": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"vue": "^2.7.16",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-vue": "^5.0.1",
|
||||
|
||||
@@ -3654,6 +3654,31 @@ class CartBundleTest(CartTestMixin, TestCase):
|
||||
assert cp.price == Decimal('0.00')
|
||||
assert b.price == Decimal('1.50')
|
||||
|
||||
@classscope(attr='orga')
|
||||
def test_voucher_apply_multiple_reduce_beyond_designated_price_no_tax_rules(self):
|
||||
self.ticket.tax_rule = None
|
||||
self.ticket.save()
|
||||
self.trans.tax_rule = None
|
||||
self.trans.save()
|
||||
cp = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=21.5, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
b = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.trans, addon_to=cp,
|
||||
price=1.5, expires=now() + timedelta(minutes=10), is_bundled=True
|
||||
)
|
||||
v = Voucher.objects.create(
|
||||
event=self.event, price_mode='set', value=Decimal('0.00'), max_usages=100
|
||||
)
|
||||
|
||||
self.cm.apply_voucher(v.code)
|
||||
self.cm.commit()
|
||||
cp.refresh_from_db()
|
||||
b.refresh_from_db()
|
||||
assert cp.price == Decimal('0.00')
|
||||
assert b.price == Decimal('1.50')
|
||||
|
||||
@classscope(attr='orga')
|
||||
def test_voucher_apply_affect_bundled(self):
|
||||
cp = CartPosition.objects.create(
|
||||
|
||||
@@ -628,7 +628,7 @@ def test_change_email(env, client):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_pw(env, client):
|
||||
def test_change_pw(env, client, client2):
|
||||
with scopes_disabled():
|
||||
customer = env[0].customers.create(email='john@example.org', is_verified=True)
|
||||
customer.set_password('foo')
|
||||
@@ -640,6 +640,12 @@ def test_change_pw(env, client):
|
||||
})
|
||||
assert r.status_code == 302
|
||||
|
||||
r = client2.post('/bigevents/account/login', {
|
||||
'email': 'john@example.org',
|
||||
'password': 'foo',
|
||||
})
|
||||
assert r.status_code == 302
|
||||
|
||||
r = client.post('/bigevents/account/password', {
|
||||
'password_current': 'invalid',
|
||||
'password': 'aYLBRNg4',
|
||||
@@ -658,6 +664,13 @@ def test_change_pw(env, client):
|
||||
customer.refresh_from_db()
|
||||
assert customer.check_password('aYLBRNg4')
|
||||
|
||||
r = client.get('/bigevents/account/password')
|
||||
assert r.status_code == 200
|
||||
|
||||
# Client 2 got logged out
|
||||
r = client2.post('/bigevents/account/password')
|
||||
assert r.status_code == 302
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_login_per_org(env, client):
|
||||
|
||||
@@ -221,7 +221,7 @@ class OrdersTest(BaseOrdersTest):
|
||||
assert not self.order.email_known_to_work
|
||||
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/open/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret, self.order.email_confirm_hash())
|
||||
'/%s/%s/order/%s/%s/open/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret, self.order.email_confirm_secret())
|
||||
)
|
||||
assert response.status_code == 302
|
||||
self.order.refresh_from_db()
|
||||
|
||||
Reference in New Issue
Block a user