mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
* Syncing fork to upstream (#1)
Sync master with master of pretix/pretix@300f8f6
* Automatically sort new products to the end
* Drop "squash your commits" from the dev guide
* Add variation descriptions and allow to order addons
* Link to Django's runserver options in dev docs
* Allow <br> tags in rich text
* Copy from event: deal with deleted items
* Make validate_cart useful together with addons
* Fix collapsing panels in the addon choice step
* Button text change if addons are present
* Update translations
* Squash migrations and bump version
* Ticket PDFs: Do not hide attendee name if code is hidden
* Add a user guide on payments
* Link PayPal and Stripe documentation in the respective forms
* Hide payment fees if they are all equal to 0.00
* Refs #39 -- New concept of "teams" (#478)
* New models
* CRUD UI
* UI for adding/removing team members
* Log display for teams
* Fix invitations, move frontend
* Drop old models (incomplete)
* Drop more old stuff
* Drop even more old stuff
* Fix tests
* Fix permission test
* flake8 fix
* Add tests fore the new code
* Rebase migrations
* Fix typo in method name
* Update translations
* Force ordering of events on dashboard
* Fix typos in events
* Prepare the pretixdroid API for an async mode in the app
* Pretixdroid tests: Ignore microseconds (chopped by mysql)
* pretixdroid API: Add related lookups
* Add idempotenty nonces to pretixdroid API
* pretixdroid: force-accepting unpaids and time display
* Marked webfonts as binary files (#487)
Webfonts now listed as binary in `.gitattributes`.
Works on pretix/pretix#486
* Fix #456 -- Allow products to be excluded from ticket-generation (#483)
* Added non-admission setting to event
`ticket_download_nonadm` now setting in storage. Still need logic for
order page/PDF generation.
Works on pretix/pretix#456.
* Download button considers `ticket_download_nonadm`
Modified Django tags to look at item admission attribute and
`ticket_download_nonadm` setting.
Works on pretix/pretix#456.
* Ticket output for non-admission disabled
PDFs/etc. will only be permitted/generated for items with the
`admission` attribute, or if the `ticket_download_nonadm` event setting
is true. Applies to single and whole-order ticket downloads.
Works on pretix/pretix#456.
* Fixed product exclusion in PDF output
Forgot PDF output was a plugin, now includes same check as base
`BaseTicketOutput.generate_order`.
Works on pretix/pretix#456
* Mail signature (#485)
* added signature field -- no function yet
* added mail signature feature
* fixed style issue
* fixed problem with signature default
* added unit test for mail signatures
* added unit test for mail signatures
* [WIP] Fix #447 -- Sendmail plugin: Create new mail based on an old one (#476)
* send old email content to the new one
* error key event
* test commit
* query bad ID
* query bad ID
* query bad ID
* query bad ID
* Update pretixdroid API version
* Refs #447 -- Extend copying old mails to subject and receipients
* Fixed bugs and added test for date range rendering (#488)
* fixed bug for same dates, added unit check for daterange
* fixed local language override in unit test
* Fix #297 -- pretixdroid: Show metrics in the control panel (#481)
* add checkin status page
add dashboard widget
add checkin page under orders
* modify checkin logic
added new fields in checkin page
added filter items
* add tests for checkins & minor improvement
* support addin_product & noadm setting logic
* remove name ordering check test case
* Fix #379 -- Add logo to event organizers (#431)
* [WIP] Add logo to event organizers.
* Fix indentation issues.
* Refactor code
Refactor code
Refactor code
* Add new migration
* Take files into account for organizer sform (settings form)
* Fix grammer
* Make bootstrap form errors specific to each fieldset
* Display logo on organizer's page
* Fix PR issues
Fix PR issues
Fix PR issues
* Reorder imports
* Remove conflicting migration
* Fix rebase conflict
* Fix #41 -- Drag-and-drop ticket editor
Undo/redo
Useful toolbox
Font selection
Add text content
Use hex for colors
JS-side dump and load
Save
Load layout, proper undo/redo
First steps to Python rendering
More PDF rendering
Copy and paste
Buttons for keyboard actions
Splash Screen
Block unbeforeunload in dirty state
Remove debugging output
Preview
Upload new PDFs via the editor
Fix bugs during PDF reload, link in settings form
New default ticket
Add OpenSans BI
Custom fonts, fix tests
* Added bootstrap-colorpicker
* Allow inline PDF display in CSP header
* Add fontpack to list of plugins
* Update German translation
* Add ticketoutputpdf's assets to MANIFEST.i
* Fix migration of old ticket styles
* Fix iCal download URL
* Multi-line location field, new field for admission time
* Admission date and time in editor
* Remove icon from "add to calendar"
* Try to fix PDF display problems in Safari
* Proxy cachedfiles that are used as editor previews
* Check Event.presale_is_running in more places
* Fix CSS generation with an empty color field
* Fix missing placeholders and reformat the sendmail view
* Fix bug that lead to wrong payment amount when switching payment method to PayPal later
* Update translation
* Revert "Syncing fork to upstream (#1)"
This reverts commit 847d409a00.
Merged wrong, my bad.
* Formatted OTP secret
New variable `secretGrouped` in `2fa_confirm_totp.html`, user-friendly
version of OTP secret (split every 4 characters).
Works on pretix/pretix#443.
* Adds manual secret entry OTP setup screen
`secretGrouped` exposed in user-friendly fashion. Includes short
instructions, copy-to-clipboard button, and js to hide instructions
unless user clicks on "Can't scan the barcode?" link.
Works on pretix/pretix#443.
* Minor indentation issuer
Fixed indentation issue (L40).
Works on pretix/pretix#443.
* Minor spacing issues
L265 of `user.py` failing flake8 tests, minor spacing fixes.
* Fixes indentation in `2fa_confirm_totp.html`
Per https://github.com/pretix/pretix/pull/490#discussion_r116165041,
fixes an issue with indentation.
Works on pretix/pretix#443, member of pretix/pretix#490.
* Removed `aria-*` attributes
Per https://github.com/pretix/pretix/pull/490#discussion_r116165115,
removes `aria` attributes from sub-tutorial.
Works on pretix/pretix#443, member of pretix/pretix#490.
* Pretix capitalization issue
Per https://github.com/pretix/pretix/pull/490#discussion_r116165193,
fixes an issue with capitalization of pretix.
Works on pretix/pretix#443, member of pretix/pretix#490.
353 lines
15 KiB
Python
353 lines
15 KiB
Python
import base64
|
|
import logging
|
|
import time
|
|
from urllib.parse import quote
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.contrib.auth import update_session_auth_hash
|
|
from django.core.urlresolvers import reverse
|
|
from django.shortcuts import get_object_or_404, redirect
|
|
from django.utils.crypto import get_random_string
|
|
from django.utils.functional import cached_property
|
|
from django.utils.http import is_safe_url
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.views.generic import FormView, TemplateView, UpdateView
|
|
from django_otp.plugins.otp_static.models import StaticDevice
|
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
|
from u2flib_server import u2f
|
|
from u2flib_server.jsapi import DeviceRegistration
|
|
|
|
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
|
|
from pretix.base.models import U2FDevice, User
|
|
from pretix.control.views.auth import get_u2f_appid
|
|
|
|
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RecentAuthenticationRequiredMixin:
|
|
max_time = 3600
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
|
|
if tdelta > self.max_time:
|
|
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
class ReauthView(TemplateView):
|
|
template_name = 'pretixcontrol/user/reauth.html'
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
password = request.POST.get("password", "")
|
|
if request.user.check_password(password):
|
|
request.session['pretix_auth_login_time'] = int(time.time())
|
|
if "next" in request.GET and is_safe_url(request.GET.get("next")):
|
|
return redirect(request.GET.get("next"))
|
|
return redirect(reverse('control:index'))
|
|
else:
|
|
messages.error(request, _('The password you entered was invalid, please try again.'))
|
|
return self.get(request, *args, **kwargs)
|
|
|
|
|
|
class UserSettings(UpdateView):
|
|
model = User
|
|
form_class = UserSettingsForm
|
|
template_name = 'pretixcontrol/user/settings.html'
|
|
|
|
def get_object(self, queryset=None):
|
|
self._old_email = self.request.user.email
|
|
return self.request.user
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['user'] = self.request.user
|
|
return kwargs
|
|
|
|
def form_invalid(self, form):
|
|
messages.error(self.request, _('Your changes could not be saved. See below for details.'))
|
|
return super().form_invalid(form)
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, _('Your changes have been saved.'))
|
|
|
|
data = {}
|
|
for k in form.changed_data:
|
|
if k not in ('old_pw', 'new_pw_repeat'):
|
|
if 'new_pw' == k:
|
|
data['new_pw'] = True
|
|
else:
|
|
data[k] = form.cleaned_data[k]
|
|
|
|
msgs = []
|
|
|
|
if 'new_pw' in form.changed_data:
|
|
msgs.append(_('Your password has been changed.'))
|
|
|
|
if 'email' in form.changed_data:
|
|
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)
|
|
|
|
sup = super().form_valid(form)
|
|
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data=data)
|
|
|
|
update_session_auth_hash(self.request, self.request.user)
|
|
return sup
|
|
|
|
def get_success_url(self):
|
|
return reverse('control:user.settings')
|
|
|
|
|
|
class UserHistoryView(TemplateView):
|
|
template_name = 'pretixcontrol/user/history.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['user'] = self.request.user
|
|
return ctx
|
|
|
|
|
|
class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
|
|
template_name = 'pretixcontrol/user/2fa_main.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data()
|
|
|
|
try:
|
|
ctx['static_tokens'] = StaticDevice.objects.get(user=self.request.user, name='emergency').token_set.all()
|
|
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['devices'] = []
|
|
for dt in REAL_DEVICE_TYPES:
|
|
objs = list(dt.objects.filter(user=self.request.user, confirmed=True))
|
|
for obj in objs:
|
|
if dt == TOTPDevice:
|
|
obj.devicetype = 'totp'
|
|
elif dt == U2FDevice:
|
|
obj.devicetype = 'u2f'
|
|
ctx['devices'] += objs
|
|
|
|
return ctx
|
|
|
|
|
|
class User2FADeviceAddView(RecentAuthenticationRequiredMixin, FormView):
|
|
form_class = User2FADeviceAddForm
|
|
template_name = 'pretixcontrol/user/2fa_add.html'
|
|
|
|
def form_valid(self, form):
|
|
if form.cleaned_data['devicetype'] == 'totp':
|
|
dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
|
|
elif form.cleaned_data['devicetype'] == 'u2f':
|
|
if not self.request.is_secure():
|
|
messages.error(self.request, _('U2F devices are only available if pretix is served via HTTPS.'))
|
|
return self.get(self.request, self.args, self.kwargs)
|
|
dev = U2FDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
|
|
return redirect(reverse('control:user.settings.2fa.confirm.' + form.cleaned_data['devicetype'], kwargs={
|
|
'device': dev.pk
|
|
}))
|
|
|
|
def form_invalid(self, form):
|
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
|
return super().form_invalid(form)
|
|
|
|
|
|
class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
|
|
template_name = 'pretixcontrol/user/2fa_delete.html'
|
|
|
|
@cached_property
|
|
def device(self):
|
|
if self.kwargs['devicetype'] == 'totp':
|
|
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
|
elif self.kwargs['devicetype'] == 'u2f':
|
|
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data()
|
|
ctx['device'] = self.device
|
|
return ctx
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.request.user.log_action('pretix.user.settings.2fa.device.deleted', user=self.request.user, data={
|
|
'id': self.device.pk,
|
|
'name': self.device.name,
|
|
'devicetype': self.kwargs['devicetype']
|
|
})
|
|
self.device.delete()
|
|
msgs = [
|
|
_('A two-factor authentication device has been removed from your account.')
|
|
]
|
|
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
|
|
self.request.user.require_2fa = False
|
|
self.request.user.save()
|
|
self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user)
|
|
msgs.append(_('Two-factor authentication has been disabled.'))
|
|
|
|
self.request.user.send_security_notice(msgs)
|
|
messages.success(request, _('The device has been removed.'))
|
|
return redirect(reverse('control:user.settings.2fa'))
|
|
|
|
|
|
class User2FADeviceConfirmU2FView(RecentAuthenticationRequiredMixin, TemplateView):
|
|
template_name = 'pretixcontrol/user/2fa_confirm_u2f.html'
|
|
|
|
@property
|
|
def app_id(self):
|
|
return get_u2f_appid(self.request)
|
|
|
|
@cached_property
|
|
def device(self):
|
|
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=False)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data()
|
|
ctx['device'] = self.device
|
|
|
|
devices = [DeviceRegistration.wrap(device.json_data)
|
|
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
|
|
enroll = u2f.start_register(self.app_id, devices)
|
|
self.request.session['_u2f_enroll'] = enroll.json
|
|
ctx['jsondata'] = enroll.json
|
|
|
|
return ctx
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
try:
|
|
binding, cert = u2f.complete_register(self.request.session.pop('_u2f_enroll'),
|
|
request.POST.get('token'),
|
|
[self.app_id])
|
|
self.device.json_data = binding.json
|
|
self.device.confirmed = True
|
|
self.device.save()
|
|
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
|
|
'id': self.device.pk,
|
|
'devicetype': 'u2f',
|
|
'name': self.device.name,
|
|
})
|
|
self.request.user.send_security_notice([
|
|
_('A new two-factor authentication device has been added to your account.')
|
|
])
|
|
|
|
note = ''
|
|
if not self.request.user.require_2fa:
|
|
note = ' ' + str(_('Please note that you still need to enable two-factor authentication for your '
|
|
'account using the buttons below to make a second factor required for logging '
|
|
'into your accont.'))
|
|
messages.success(request, str(_('The device has been verified and can now be used.')) + note)
|
|
return redirect(reverse('control:user.settings.2fa'))
|
|
except Exception:
|
|
messages.error(request, _('The registration could not be completed. Please try again.'))
|
|
logger.exception('U2F registration failed')
|
|
return redirect(reverse('control:user.settings.2fa.confirm.u2f', kwargs={
|
|
'device': self.device.pk
|
|
}))
|
|
|
|
|
|
class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateView):
|
|
template_name = 'pretixcontrol/user/2fa_confirm_totp.html'
|
|
|
|
@cached_property
|
|
def device(self):
|
|
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=False)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data()
|
|
|
|
ctx['secret'] = base64.b32encode(self.device.bin_key).decode('utf-8')
|
|
ctx['secretGrouped'] = " ".join([ctx['secret'].lower()[(i * 4): (i + 1) * 4] for i in range(len(ctx['secret']) // 4)])
|
|
ctx['qrdata'] = 'otpauth://totp/{label}%3A%20{user}?issuer={label}&secret={secret}&digits={digits}'.format(
|
|
label=quote(settings.PRETIX_INSTANCE_NAME), user=quote(self.request.user.email),
|
|
secret=ctx['secret'],
|
|
digits=self.device.digits
|
|
)
|
|
ctx['device'] = self.device
|
|
return ctx
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
token = request.POST.get('token', '')
|
|
if self.device.verify_token(token):
|
|
self.device.confirmed = True
|
|
self.device.save()
|
|
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
|
|
'id': self.device.pk,
|
|
'name': self.device.name,
|
|
'devicetype': 'totp'
|
|
})
|
|
self.request.user.send_security_notice([
|
|
_('A new two-factor authentication device has been added to your account.')
|
|
])
|
|
|
|
note = ''
|
|
if not self.request.user.require_2fa:
|
|
note = ' ' + str(_('Please note that you still need to enable two-factor authentication for your '
|
|
'account using the buttons below to make a second factor required for logging '
|
|
'into your accont.'))
|
|
messages.success(request, str(_('The device has been verified and can now be used.')) + note)
|
|
return redirect(reverse('control:user.settings.2fa'))
|
|
else:
|
|
messages.error(request, _('The code you entered was not valid. If this problem persists, please check '
|
|
'that the date and time of your phone are configured correctly.'))
|
|
return redirect(reverse('control:user.settings.2fa.confirm.totp', kwargs={
|
|
'device': self.device.pk
|
|
}))
|
|
|
|
|
|
class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
|
|
template_name = 'pretixcontrol/user/2fa_enable.html'
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
|
|
messages.error(request, _('Please configure at least one device before enabling two-factor '
|
|
'authentication.'))
|
|
return redirect(reverse('control:user.settings.2fa'))
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.request.user.require_2fa = True
|
|
self.request.user.save()
|
|
self.request.user.log_action('pretix.user.settings.2fa.enabled', user=self.request.user)
|
|
messages.success(request, _('Two-factor authentication is now enabled for your account.'))
|
|
self.request.user.send_security_notice([
|
|
_('Two-factor authentication has been enabled.')
|
|
])
|
|
return redirect(reverse('control:user.settings.2fa'))
|
|
|
|
|
|
class User2FADisableView(RecentAuthenticationRequiredMixin, TemplateView):
|
|
template_name = 'pretixcontrol/user/2fa_disable.html'
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.request.user.require_2fa = False
|
|
self.request.user.save()
|
|
self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user)
|
|
messages.success(request, _('Two-factor authentication is now disabled for your account.'))
|
|
self.request.user.send_security_notice([
|
|
_('Two-factor authentication has been disabled.')
|
|
])
|
|
return redirect(reverse('control:user.settings.2fa'))
|
|
|
|
|
|
class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, TemplateView):
|
|
template_name = 'pretixcontrol/user/2fa_regenemergency.html'
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
d = StaticDevice.objects.get(user=self.request.user, name='emergency')
|
|
d.token_set.all().delete()
|
|
for i in range(10):
|
|
d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
|
|
self.request.user.log_action('pretix.user.settings.2fa.regenemergency', user=self.request.user)
|
|
self.request.user.send_security_notice([
|
|
_('Your two-factor emergency codes have been regenerated.')
|
|
])
|
|
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.'))
|
|
return redirect(reverse('control:user.settings.2fa'))
|