mirror of
https://github.com/pretix/pretix.git
synced 2026-08-26 13:14:40 +00:00
Improve email and password change forms, implement is_verified flag
This commit is contained in:
@@ -214,21 +214,36 @@ class PasswordRecoverForm(forms.Form):
|
|||||||
error_messages = {
|
error_messages = {
|
||||||
'pw_mismatch': _("Please enter the same password twice"),
|
'pw_mismatch': _("Please enter the same password twice"),
|
||||||
}
|
}
|
||||||
|
email = forms.EmailField(max_length=255,
|
||||||
|
disabled=True,
|
||||||
|
label=_("Your email address"),
|
||||||
|
widget=forms.EmailInput(
|
||||||
|
attrs={'autocomplete': 'username'},
|
||||||
|
))
|
||||||
password = forms.CharField(
|
password = forms.CharField(
|
||||||
label=_('Password'),
|
label=_('Password'),
|
||||||
widget=forms.PasswordInput,
|
widget=forms.PasswordInput(attrs={
|
||||||
|
'autocomplete': 'new-password',
|
||||||
|
}),
|
||||||
max_length=4096,
|
max_length=4096,
|
||||||
required=True
|
required=True
|
||||||
)
|
)
|
||||||
password_repeat = forms.CharField(
|
password_repeat = forms.CharField(
|
||||||
label=_('Repeat password'),
|
label=_('Repeat password'),
|
||||||
widget=forms.PasswordInput,
|
widget=forms.PasswordInput(attrs={
|
||||||
|
'autocomplete': 'new-password',
|
||||||
|
}),
|
||||||
max_length=4096,
|
max_length=4096,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, user_id=None, *args, **kwargs):
|
def __init__(self, user_id=None, *args, **kwargs):
|
||||||
self.user_id = user_id
|
try:
|
||||||
super().__init__(*args, **kwargs)
|
self.user = User.objects.get(id=user_id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
self.user = None
|
||||||
|
initial = kwargs.pop('initial', {})
|
||||||
|
initial['email'] = self.user.email
|
||||||
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
password1 = self.cleaned_data.get('password', '')
|
password1 = self.cleaned_data.get('password', '')
|
||||||
@@ -243,11 +258,7 @@ class PasswordRecoverForm(forms.Form):
|
|||||||
|
|
||||||
def clean_password(self):
|
def clean_password(self):
|
||||||
password1 = self.cleaned_data.get('password', '')
|
password1 = self.cleaned_data.get('password', '')
|
||||||
try:
|
if validate_password(password1, user=self.user) is not None:
|
||||||
user = User.objects.get(id=self.user_id)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
user = None
|
|
||||||
if validate_password(password1, user=user) is not None:
|
|
||||||
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
|
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
|
||||||
return password1
|
return password1
|
||||||
|
|
||||||
@@ -307,3 +318,11 @@ class ReauthForm(forms.Form):
|
|||||||
self.error_messages['inactive'],
|
self.error_messages['inactive'],
|
||||||
code='inactive',
|
code='inactive',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmationCodeForm(forms.Form):
|
||||||
|
code = forms.IntegerField(
|
||||||
|
label='',
|
||||||
|
widget=forms.NumberInput(attrs={'class': 'confirmation-code-input', 'inputmode': 'numeric', 'type': 'text'}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -39,37 +39,17 @@ from django.contrib.auth.password_validation import (
|
|||||||
password_validators_help_texts, validate_password,
|
password_validators_help_texts, validate_password,
|
||||||
)
|
)
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.urls.base import reverse
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
from pretix.control.forms import SingleLanguageWidget
|
from pretix.control.forms import SingleLanguageWidget
|
||||||
|
from pretix.helpers.format import format_map
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsForm(forms.ModelForm):
|
class UserSettingsForm(forms.ModelForm):
|
||||||
error_messages = {
|
|
||||||
'duplicate_identifier': _("There already is an account associated with this email address. "
|
|
||||||
"Please choose a different one."),
|
|
||||||
'pw_current': _("Please enter your current password if you want to change your email address "
|
|
||||||
"or password."),
|
|
||||||
'pw_current_wrong': _("The current password you entered was not correct."),
|
|
||||||
'pw_mismatch': _("Please enter the same password twice"),
|
|
||||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
|
||||||
'pw_equal': _("Please choose a password different to your current one.")
|
|
||||||
}
|
|
||||||
|
|
||||||
old_pw = forms.CharField(max_length=255,
|
|
||||||
required=False,
|
|
||||||
label=_("Your current password"),
|
|
||||||
widget=forms.PasswordInput())
|
|
||||||
new_pw = forms.CharField(max_length=255,
|
|
||||||
required=False,
|
|
||||||
label=_("New password"),
|
|
||||||
widget=forms.PasswordInput())
|
|
||||||
new_pw_repeat = forms.CharField(max_length=255,
|
|
||||||
required=False,
|
|
||||||
label=_("Repeat new password"),
|
|
||||||
widget=forms.PasswordInput())
|
|
||||||
timezone = forms.ChoiceField(
|
timezone = forms.ChoiceField(
|
||||||
choices=((a, a) for a in common_timezones),
|
choices=((a, a) for a in common_timezones),
|
||||||
label=_("Default timezone"),
|
label=_("Default timezone"),
|
||||||
@@ -93,11 +73,75 @@ class UserSettingsForm(forms.ModelForm):
|
|||||||
self.user = kwargs.pop('user')
|
self.user = kwargs.pop('user')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['email'].required = True
|
self.fields['email'].required = True
|
||||||
if self.user.auth_backend != 'native':
|
self.fields['email'].disabled = True
|
||||||
del self.fields['old_pw']
|
self.fields['email'].help_text = format_map('<a href="{link}"><span class="fa fa-edit"></span> {text}</a>', {
|
||||||
del self.fields['new_pw']
|
'text': _("Change email address"),
|
||||||
del self.fields['new_pw_repeat']
|
'link': reverse('control:user.settings.email.change')
|
||||||
self.fields['email'].disabled = True
|
})
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
password1 = self.cleaned_data.get('new_pw')
|
||||||
|
old_pw = self.cleaned_data.get('old_pw')
|
||||||
|
|
||||||
|
if password1 and password1 == old_pw:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
self.error_messages['pw_equal'],
|
||||||
|
code='pw_equal'
|
||||||
|
)
|
||||||
|
|
||||||
|
if password1:
|
||||||
|
self.instance.set_password(password1)
|
||||||
|
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class User2FADeviceAddForm(forms.Form):
|
||||||
|
name = forms.CharField(label=_('Device name'), max_length=64)
|
||||||
|
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
||||||
|
('totp', _('Smartphone with the Authenticator application')),
|
||||||
|
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class UserPasswordChangeForm(forms.Form):
|
||||||
|
error_messages = {
|
||||||
|
'pw_current': _("Please enter your current password if you want to change your email address "
|
||||||
|
"or password."),
|
||||||
|
'pw_current_wrong': _("The current password you entered was not correct."),
|
||||||
|
'pw_mismatch': _("Please enter the same password twice"),
|
||||||
|
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||||
|
'pw_equal': _("Please choose a password different to your current one.")
|
||||||
|
}
|
||||||
|
email = forms.EmailField(max_length=255,
|
||||||
|
disabled=True,
|
||||||
|
label=_("Your email address"),
|
||||||
|
widget=forms.EmailInput(
|
||||||
|
attrs={'autocomplete': 'username'},
|
||||||
|
))
|
||||||
|
old_pw = forms.CharField(max_length=255,
|
||||||
|
required=False,
|
||||||
|
label=_("Your current password"),
|
||||||
|
widget=forms.PasswordInput(
|
||||||
|
attrs={'autocomplete': 'current-password'},
|
||||||
|
))
|
||||||
|
new_pw = forms.CharField(max_length=255,
|
||||||
|
required=False,
|
||||||
|
label=_("New password"),
|
||||||
|
widget=forms.PasswordInput(
|
||||||
|
attrs={'autocomplete': 'new-password'},
|
||||||
|
))
|
||||||
|
new_pw_repeat = forms.CharField(max_length=255,
|
||||||
|
required=False,
|
||||||
|
label=_("Repeat new password"),
|
||||||
|
widget=forms.PasswordInput(
|
||||||
|
attrs={'autocomplete': 'new-password'},
|
||||||
|
))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.user = kwargs.pop('user')
|
||||||
|
initial = kwargs.pop('initial', {})
|
||||||
|
initial['email'] = self.user.email
|
||||||
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
|
||||||
def clean_old_pw(self):
|
def clean_old_pw(self):
|
||||||
old_pw = self.cleaned_data.get('old_pw')
|
old_pw = self.cleaned_data.get('old_pw')
|
||||||
@@ -121,15 +165,6 @@ class UserSettingsForm(forms.ModelForm):
|
|||||||
|
|
||||||
return old_pw
|
return old_pw
|
||||||
|
|
||||||
def clean_email(self):
|
|
||||||
email = self.cleaned_data['email']
|
|
||||||
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.instance.pk)).exists():
|
|
||||||
raise forms.ValidationError(
|
|
||||||
self.error_messages['duplicate_identifier'],
|
|
||||||
code='duplicate_identifier',
|
|
||||||
)
|
|
||||||
return email
|
|
||||||
|
|
||||||
def clean_new_pw(self):
|
def clean_new_pw(self):
|
||||||
password1 = self.cleaned_data.get('new_pw', '')
|
password1 = self.cleaned_data.get('new_pw', '')
|
||||||
if password1 and validate_password(password1, user=self.user) is not None:
|
if password1 and validate_password(password1, user=self.user) is not None:
|
||||||
@@ -148,32 +183,24 @@ class UserSettingsForm(forms.ModelForm):
|
|||||||
code='pw_mismatch'
|
code='pw_mismatch'
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
password1 = self.cleaned_data.get('new_pw')
|
|
||||||
email = self.cleaned_data.get('email')
|
|
||||||
old_pw = self.cleaned_data.get('old_pw')
|
|
||||||
|
|
||||||
if (password1 or email != self.user.email) and not old_pw:
|
|
||||||
|
class UserEmailChangeForm(forms.Form):
|
||||||
|
error_messages = {
|
||||||
|
'duplicate_identifier': _("There already is an account associated with this email address. "
|
||||||
|
"Please choose a different one."),
|
||||||
|
}
|
||||||
|
new_email = forms.EmailField(label=_('New email address'))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.user = kwargs.pop('user')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean_new_email(self):
|
||||||
|
email = self.cleaned_data['new_email']
|
||||||
|
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.user.pk)).exists():
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['pw_current'],
|
self.error_messages['duplicate_identifier'],
|
||||||
code='pw_current'
|
code='duplicate_identifier',
|
||||||
)
|
)
|
||||||
|
return email
|
||||||
if password1 and password1 == old_pw:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
self.error_messages['pw_equal'],
|
|
||||||
code='pw_equal'
|
|
||||||
)
|
|
||||||
|
|
||||||
if password1:
|
|
||||||
self.instance.set_password(password1)
|
|
||||||
|
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
|
|
||||||
class User2FADeviceAddForm(forms.Form):
|
|
||||||
name = forms.CharField(label=_('Device name'), max_length=64)
|
|
||||||
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
|
||||||
('totp', _('Smartphone with the Authenticator application')),
|
|
||||||
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
|
|
||||||
))
|
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 4.2.23 on 2025-09-04 12:58
|
# Generated by Django 4.2.23 on 2025-09-04 16:06
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ class Migration(migrations.Migration):
|
|||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="user",
|
model_name="user",
|
||||||
name="verified_email",
|
name="is_verified",
|
||||||
field=models.EmailField(max_length=190, null=True),
|
field=models.BooleanField(default=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
import binascii
|
import binascii
|
||||||
import json
|
import json
|
||||||
import operator
|
import operator
|
||||||
|
import random
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
@@ -243,7 +244,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
|
|
||||||
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
|
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
|
||||||
verbose_name=_('Email'), max_length=190)
|
verbose_name=_('Email'), max_length=190)
|
||||||
verified_email = models.EmailField(null=True, blank=True, verbose_name=_('Verified Email'), max_length=190)
|
is_verified = models.BooleanField(default=True, verbose_name=_('Verified email address'))
|
||||||
fullname = models.CharField(max_length=255, blank=True, null=True,
|
fullname = models.CharField(max_length=255, blank=True, null=True,
|
||||||
verbose_name=_('Full name'))
|
verbose_name=_('Full name'))
|
||||||
is_active = models.BooleanField(default=True,
|
is_active = models.BooleanField(default=True,
|
||||||
@@ -355,6 +356,43 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
except SendMailException:
|
except SendMailException:
|
||||||
pass # Already logged
|
pass # Already logged
|
||||||
|
|
||||||
|
def send_confirmation_code(self, reason, email=None):
|
||||||
|
from pretix.base.services.mail import mail
|
||||||
|
|
||||||
|
with language(self.locale):
|
||||||
|
if reason == 'email_change':
|
||||||
|
msg = str(_('to confirm changing your email address from {old_email}\nto {new_email}, use the following code:').format(
|
||||||
|
old_email=self.email, new_email=email,
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid confirmation code reason')
|
||||||
|
|
||||||
|
code = "%07d" % random.randint(0, 9999999)
|
||||||
|
cache.set('user_confirmation_code:' + str(self.pk), code + ':' + reason + ':' + str(email), 1800)
|
||||||
|
|
||||||
|
mail(
|
||||||
|
email or self.email,
|
||||||
|
_('pretix confirmation code'),
|
||||||
|
'pretixcontrol/email/confirmation_code.txt',
|
||||||
|
{
|
||||||
|
'user': self,
|
||||||
|
'reason': msg,
|
||||||
|
'code': code,
|
||||||
|
},
|
||||||
|
event=None,
|
||||||
|
user=self,
|
||||||
|
locale=self.locale
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_confirmation_code(self, reason, code):
|
||||||
|
stored = cache.get('user_confirmation_code:' + str(self.pk))
|
||||||
|
if not stored:
|
||||||
|
return None
|
||||||
|
stored_code, stored_reason, email = stored.split(":", maxsplit=2)
|
||||||
|
if int(stored_code) == int(code) and stored_reason == reason:
|
||||||
|
return email
|
||||||
|
|
||||||
|
|
||||||
def send_password_reset(self):
|
def send_password_reset(self):
|
||||||
from pretix.base.services.mail import mail
|
from pretix.base.services.mail import mail
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends "pretixcontrol/auth/base.html" %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block content %}
|
||||||
|
<form class="form-signin" action="" method="post">
|
||||||
|
<h3>{% trans "Enter confirmation code" %}</h3>
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
{% bootstrap_form_errors form type='all' layout='inline' %}
|
||||||
|
{% bootstrap_field form.code %}
|
||||||
|
<div class="form-group buttons">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% trans "Okay" %}
|
||||||
|
</button>
|
||||||
|
<a href="{{ cancel_url }}" class="btn btn-default">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<h3>{% trans "Set new password" %}</h3>
|
<h3>{% trans "Set new password" %}</h3>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_form_errors form type='all' layout='inline' %}
|
{% bootstrap_form_errors form type='all' layout='inline' %}
|
||||||
|
{% bootstrap_field form.email %}
|
||||||
{% bootstrap_field form.password %}
|
{% bootstrap_field form.password %}
|
||||||
{% bootstrap_field form.password_repeat %}
|
{% bootstrap_field form.password_repeat %}
|
||||||
<div class="form-group buttons">
|
<div class="form-group buttons">
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
|
||||||
|
|
||||||
|
{{ reason }}
|
||||||
|
|
||||||
|
{{ code }}
|
||||||
|
|
||||||
|
Please do never give this code to another person. Our support team will never ask for this code.
|
||||||
|
|
||||||
|
If this code was not requested by you, please contact us immediately.
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
Your pretix team
|
||||||
|
{% endblocktrans %}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends "pretixcontrol/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block title %}{% trans "Change login email address" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<form action="" method="post" class="form centered-form">
|
||||||
|
<h1>
|
||||||
|
{% trans "Change login email address" %}
|
||||||
|
</h1>
|
||||||
|
<br>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form_errors form %}
|
||||||
|
{% bootstrap_field form.new_email %}
|
||||||
|
<div class="text-right">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save btn-lg">
|
||||||
|
{% trans "Continue" %}
|
||||||
|
</button>
|
||||||
|
<a href="{% url "control:user.settings" %}" class="btn btn-default btn-lg">{% trans "Cancel" %}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends "pretixcontrol/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% block title %}{% trans "Change password" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<form action="" method="post" class="form centered-form">
|
||||||
|
<h1>
|
||||||
|
{% trans "Change password" %}
|
||||||
|
</h1>
|
||||||
|
<br>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form_errors form %}
|
||||||
|
{% bootstrap_field form.email %}
|
||||||
|
{% bootstrap_field form.old_pw %}
|
||||||
|
{% bootstrap_field form.new_pw %}
|
||||||
|
{% bootstrap_field form.new_pw_repeat %}
|
||||||
|
<div class="text-right">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save btn-lg">
|
||||||
|
{% trans "Change password" %}
|
||||||
|
</button>
|
||||||
|
<a href="{% url "control:user.settings" %}" class="btn btn-default btn-lg">{% trans "Cancel" %}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -46,8 +46,18 @@
|
|||||||
{% bootstrap_field form.new_pw layout='horizontal' %}
|
{% bootstrap_field form.new_pw layout='horizontal' %}
|
||||||
{% bootstrap_field form.new_pw_repeat layout='horizontal' %}
|
{% bootstrap_field form.new_pw_repeat layout='horizontal' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user.auth_backend == 'native' %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label">{% trans "Password" %}</label>
|
||||||
|
<div class="col-md-9 static-form-row">
|
||||||
|
<a href="{% url "control:user.settings.password.change" %}">
|
||||||
|
<span class="fa fa-edit"></span> {% trans "Change password" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Two-factor authentication" %}</label>
|
<label class="col-md-3 control-label">{% trans "Two-factor authentication" %}</label>
|
||||||
<div class="col-md-9 static-form-row">
|
<div class="col-md-9 static-form-row">
|
||||||
{% if user.require_2fa %}
|
{% if user.require_2fa %}
|
||||||
<span class="label label-success">{% trans "Enabled" %}</span>
|
<span class="label label-success">{% trans "Enabled" %}</span>
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ urlpatterns = [
|
|||||||
name='user.settings.2fa.confirm.webauthn'),
|
name='user.settings.2fa.confirm.webauthn'),
|
||||||
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
|
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
|
||||||
name='user.settings.2fa.delete'),
|
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'^settings/email/confirm$', user.UserEmailConfirmView.as_view(), name='user.settings.email.confirm'),
|
||||||
|
re_path(r'^settings/email/change$', user.UserEmailChangeView.as_view(), name='user.settings.email.change'),
|
||||||
|
re_path(r'^settings/password/change$', user.UserPasswordChangeView.as_view(), name='user.settings.password.change'),
|
||||||
re_path(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
|
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/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
|
||||||
re_path(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'),
|
re_path(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'),
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ from webauthn.helpers import generate_challenge, generate_user_handle
|
|||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from pretix.base.auth import get_auth_backends
|
from pretix.base.auth import get_auth_backends
|
||||||
from pretix.base.forms.auth import ReauthForm
|
from pretix.base.forms.auth import ConfirmationCodeForm, ReauthForm
|
||||||
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
|
from pretix.base.forms.user import User2FADeviceAddForm, UserEmailChangeForm, UserPasswordChangeForm, UserSettingsForm
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Event, LogEntry, NotificationSetting, U2FDevice, User, WebAuthnDevice,
|
Event, LogEntry, NotificationSetting, U2FDevice, User, WebAuthnDevice,
|
||||||
)
|
)
|
||||||
@@ -240,28 +240,7 @@ class UserSettings(UpdateView):
|
|||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
for k in form.changed_data:
|
for k in form.changed_data:
|
||||||
if k not in ('old_pw', 'new_pw_repeat'):
|
data[k] = form.cleaned_data[k]
|
||||||
if 'new_pw' == k:
|
|
||||||
data['new_pw'] = True
|
|
||||||
else:
|
|
||||||
data[k] = form.cleaned_data[k]
|
|
||||||
|
|
||||||
msgs = []
|
|
||||||
|
|
||||||
if 'new_pw' in form.changed_data:
|
|
||||||
self.request.user.needs_password_change = False
|
|
||||||
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:
|
|
||||||
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)
|
sup = super().form_valid(form)
|
||||||
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data=data)
|
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data=data)
|
||||||
@@ -842,26 +821,94 @@ class EditStaffSession(StaffMemberRequiredMixin, UpdateView):
|
|||||||
return get_object_or_404(StaffSession, pk=self.kwargs['id'], user=self.request.user)
|
return get_object_or_404(StaffSession, pk=self.kwargs['id'], user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class ConfirmEmailView(View):
|
class UserPasswordChangeView(FormView):
|
||||||
|
max_time = 300
|
||||||
|
|
||||||
def get(self, request, token, *args, **kwargs):
|
form_class = UserPasswordChangeForm
|
||||||
try:
|
template_name = 'pretixcontrol/user/change_password.html'
|
||||||
uid = int(token.split("-")[0])
|
|
||||||
except ValueError:
|
def get_form_kwargs(self):
|
||||||
uid = None
|
return {
|
||||||
if uid and compare_digest(cache.get('confirm_email_token:' + str(uid)), token):
|
**super().get_form_kwargs(),
|
||||||
user = User.objects.get(pk=uid)
|
"user": self.request.user,
|
||||||
with transaction.atomic():
|
}
|
||||||
if user.email != user.verified_email:
|
|
||||||
user.log_action("user.email.confirmed", data={
|
def form_valid(self, form):
|
||||||
"old_email": user.verified_email,
|
with transaction.atomic():
|
||||||
"new_email": user.email,
|
self.request.user.set_password(form.cleaned_data['new_pw'])
|
||||||
})
|
self.request.user.needs_password_change = False
|
||||||
user.verified_email = user.email
|
self.request.user.save()
|
||||||
user.save()
|
msgs = []
|
||||||
messages.success(request, _('Your email has been confirmed.'))
|
msgs.append(_('Your password has been changed.'))
|
||||||
else:
|
self.request.user.send_security_notice(msgs)
|
||||||
messages.success(request, _('Your email was already confirmed.'))
|
|
||||||
else:
|
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data={'new_pw': True})
|
||||||
messages.error(request, _('Invalid confirmation link. Please try again.'))
|
|
||||||
return redirect("control:user.settings")
|
update_session_auth_hash(self.request, self.request.user)
|
||||||
|
return redirect(reverse('control:user.settings', kwargs={}))
|
||||||
|
|
||||||
|
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 UserEmailChangeView(RecentAuthenticationRequiredMixin, FormView):
|
||||||
|
max_time = 300
|
||||||
|
|
||||||
|
form_class = UserEmailChangeForm
|
||||||
|
template_name = 'pretixcontrol/user/change_email.html'
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
return {
|
||||||
|
**super().get_form_kwargs(),
|
||||||
|
"user": self.request.user,
|
||||||
|
}
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.request.user.send_confirmation_code('email_change', form.cleaned_data['new_email'])
|
||||||
|
return redirect(reverse('control:user.settings.email.confirm', kwargs={}))
|
||||||
|
|
||||||
|
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 UserEmailConfirmView(FormView):
|
||||||
|
form_class = ConfirmationCodeForm
|
||||||
|
template_name = 'pretixcontrol/auth/confirmation_code.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return {
|
||||||
|
**super().get_context_data(**kwargs),
|
||||||
|
"cancel_url": reverse('control:user.settings', kwargs={}),
|
||||||
|
"message": _("Please enter the confirmation code we sent to your new email address:"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def form_valid(self, form):
|
||||||
|
new_email = self.request.user.check_confirmation_code('email_change', form.cleaned_data['code'])
|
||||||
|
if not new_email:
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
msgs = []
|
||||||
|
msgs.append(_('Your email address has been changed to {email}.').format(email=new_email))
|
||||||
|
old_email = self.request.user.email
|
||||||
|
self.request.user.send_security_notice(msgs, email=old_email)
|
||||||
|
self.request.user.send_security_notice(msgs, email=new_email)
|
||||||
|
|
||||||
|
self.request.user.email = new_email
|
||||||
|
self.request.user.is_verified = True
|
||||||
|
self.request.user.save()
|
||||||
|
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data={
|
||||||
|
'old_email': old_email,
|
||||||
|
'email': new_email,
|
||||||
|
'email_verified': True,
|
||||||
|
})
|
||||||
|
update_session_auth_hash(self.request, self.request.user)
|
||||||
|
|
||||||
|
messages.success(self.request, _('Your email address has been changed successfully.'))
|
||||||
|
return redirect(reverse('control:user.settings', kwargs={}))
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||||
|
return super().form_invalid(form)
|
||||||
|
|||||||
@@ -263,3 +263,11 @@ svg.svg-icon {
|
|||||||
@include table-row-variant('warning', var(--pretix-brand-warning-lighten-40), var(--pretix-brand-warning-lighten-35));
|
@include table-row-variant('warning', var(--pretix-brand-warning-lighten-40), var(--pretix-brand-warning-lighten-35));
|
||||||
@include table-row-variant('danger', var(--pretix-brand-danger-lighten-30), var(--pretix-brand-danger-lighten-25));
|
@include table-row-variant('danger', var(--pretix-brand-danger-lighten-30), var(--pretix-brand-danger-lighten-25));
|
||||||
|
|
||||||
|
.confirmation-code-input {
|
||||||
|
font-size: 200%;
|
||||||
|
font-family: monospace;
|
||||||
|
font-stretch: expanded;
|
||||||
|
text-align: center;
|
||||||
|
height: 50px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -936,3 +936,11 @@ details {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (min-width: $screen-lg-min) {
|
||||||
|
.centered-form {
|
||||||
|
margin: 80px auto;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user