Allow users to provide their email or username for password reset process (#49131)

This commit is contained in:
Benjamin Renard 2020-12-17 11:31:43 +01:00 committed by Benjamin Dauvergne
parent 02f00a2046
commit fd248ebb89
4 changed files with 36 additions and 7 deletions

View File

@ -19,6 +19,7 @@ from collections import OrderedDict
from django.contrib.auth import forms as auth_forms
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.forms import Form
from django import forms
from django.utils.translation import ugettext_lazy as _
@ -35,7 +36,7 @@ logger = logging.getLogger(__name__)
class PasswordResetForm(forms.Form):
next_url = forms.CharField(widget=forms.HiddenInput, required=False)
email = ValidatedEmailField(
email = forms.CharField(
label=_("Email"), max_length=254)
def save(self):
@ -45,7 +46,8 @@ class PasswordResetForm(forms.Form):
"""
email = self.cleaned_data["email"].strip()
users = get_user_queryset()
active_users = users.filter(email__iexact=email, deleted__isnull=True)
active_users = users.filter(
Q(email__iexact=email) | Q(username__iexact=email), deleted__isnull=True)
for user in active_users:
# we don't set the password to a random string, as some users should not have
# a password

View File

@ -806,8 +806,8 @@ def build_reset_password_url(user, request=None, next_url=None, set_random_passw
user.save()
lifetime = settings.PASSWORD_RESET_TIMEOUT_DAYS * 3600 * 24
# invalidate any token associated with this user
Token.objects.filter(kind='pw-reset', content__user=user.pk, content__email=user.email).delete()
token = Token.create('pw-reset', {'user': user.pk, 'email': user.email}, duration=lifetime)
Token.objects.filter(kind='pw-reset', content__user=user.pk, content__email=user.email, content__username=user.username).delete()
token = Token.create('pw-reset', {'user': user.pk, 'email': user.email, 'username': user.username}, duration=lifetime)
reset_url = make_url(
'password_reset_confirm',
kwargs={'token': token.uuid_b64url},

View File

@ -673,7 +673,8 @@ class PasswordResetView(FormView):
# if an email has already been sent, warn once before allowing resend
token = models.Token.objects.filter(
kind='pw-reset', content__email=email, expires__gt=timezone.now()
Q(content__email__iexact=email) | Q(content__username__iexact=email),
kind='pw-reset', expires__gt=timezone.now()
).exists()
resend_key = 'pw-reset-allow-resend'
if app_settings.A2_TOKEN_EXISTS_WARNING and token and not self.request.session.get(resend_key):

View File

@ -41,7 +41,7 @@ def test_send_password_reset_email(app, simple_user, mailoutbox):
utils.assert_event('user.password.reset', user=simple_user, session=app.session)
def test_view(app, simple_user, mailoutbox, settings):
def test_view_with_email(app, simple_user, mailoutbox, settings):
url = reverse('password_reset')
resp = app.get(url, status=200)
resp.form.set('email', simple_user.email)
@ -51,7 +51,33 @@ def test_view(app, simple_user, mailoutbox, settings):
utils.assert_event('user.password.reset.request', user=simple_user, email=simple_user.email)
assert resp['Location'].endswith('/instructions/')
resp = resp.follow()
assert simple_user.email in resp.text
assert '"noreply@example.net"' in resp.text
assert 'show only addr' not in resp.text
assert len(mailoutbox) == 1
url = utils.get_link_from_mail(mailoutbox[0])
relative_url = url.split('testserver')[1]
resp = app.get(relative_url, status=200)
resp.form.set('new_password1', '1234==aA')
resp.form.set('new_password2', '1234==aA')
resp = resp.form.submit()
# verify user is logged
assert str(app.session['_auth_user_id']) == str(simple_user.pk)
with override_settings(A2_USER_CAN_RESET_PASSWORD=False):
url = reverse('password_reset')
app.get(url, status=404)
def test_view_with_username(app, simple_user, mailoutbox, settings):
url = reverse('password_reset')
resp = app.get(url, status=200)
resp.form.set('email', simple_user.username)
assert len(mailoutbox) == 0
settings.DEFAULT_FROM_EMAIL = 'show only addr <noreply@example.net>'
resp = resp.form.submit()
utils.assert_event('user.password.reset.request', user=simple_user, email=simple_user.email)
assert resp['Location'].endswith('/instructions/')
resp = resp.follow()
assert '"noreply@example.net"' in resp.text
assert 'show only addr' not in resp.text
assert len(mailoutbox) == 1