Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accounts: For signup, login and password reset, username and password should be treated case insensitive #2067

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
4 changes: 2 additions & 2 deletions mezzanine/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def clean_email(self):
Ensure the email address is not already registered.
"""
email = self.cleaned_data.get("email")
qs = User.objects.exclude(id=self.instance.id).filter(email=email)
qs = User.objects.exclude(id=self.instance.id).filter(email__iexact=email)
if len(qs) == 0:
return email
raise forms.ValidationError(gettext("This email is already registered"))
Expand Down Expand Up @@ -261,7 +261,7 @@ class PasswordResetForm(Html5Mixin, forms.Form):

def clean(self):
username = self.cleaned_data.get("username")
username_or_email = Q(username=username) | Q(email=username)
username_or_email = Q(username__iexact=username) | Q(email__iexact=username)
try:
user = User.objects.get(username_or_email, is_active=True)
except User.DoesNotExist:
Expand Down
4 changes: 3 additions & 1 deletion mezzanine/core/auth_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class MezzanineBackend(ModelBackend):
Args are either ``username`` and ``password``, or ``uidb36``
and ``token``. In either case, ``is_active`` can also be given.

Usernames and Email addresses are not case sensitive

For login, is_active is not given, so that the login form can
raise a specific error for inactive users.
For password reset, True is given for is_active.
Expand All @@ -25,7 +27,7 @@ def authenticate(self, *args, **kwargs):
if kwargs:
username = kwargs.pop("username", None)
if username:
username_or_email = Q(username=username) | Q(email=username)
username_or_email = Q(username__iexact=username) | Q(email__iexact=username)
password = kwargs.pop("password", None)
try:
user = User.objects.get(username_or_email, **kwargs)
Expand Down
7 changes: 6 additions & 1 deletion mezzanine/core/templatetags/mezzanine_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,12 +429,17 @@ def thumbnail(
pad_container = Image.new("RGBA", pad_size, padding_color)
pad_container.paste(image, (pad_left, pad_top))
image = pad_container
# Make thumbnail a PNG - required if original isn't one
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this change probably doesn't need to go back to main, I'm sure there's a better solution to changing a JPG's thumbnail to PNG (to allow for transparency background when using padding). Feel free to skip these lines on merge

if filetype != "PNG":
filetype = "PNG"
thumb_path += ".png"
thumb_url += ".png"

# Create the thumbnail.
to_size = (to_width, to_height)
to_pos = (left, top)
try:
image = ImageOps.fit(image, to_size, Image.ANTIALIAS, 0, to_pos)
image = ImageOps.fit(image, to_size, Image.LANCZOS, 0, to_pos)
image = image.save(thumb_path, filetype, quality=quality, **image_info)
# Push a remote copy of the thumbnail if MEDIA_URL is
# absolute.
Expand Down
2 changes: 1 addition & 1 deletion mezzanine/utils/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def escape(html):
strip=True,
strip_comments=False,
css_sanitizer=css_sanitizer,
protocols=ALLOWED_PROTOCOLS + ["tel"],
protocols=list(ALLOWED_PROTOCOLS) + ["tel"],
)


Expand Down
163 changes: 161 additions & 2 deletions tests/test_accounts.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from django.contrib.auth import get_user_model
import django
from django.contrib.auth import get_user, get_user_model
from django.contrib.auth.tokens import default_token_generator
from django.core import mail
from django.forms.fields import DateField, DateTimeField
from django.urls import reverse
from django.utils.http import int_to_base36

from mezzanine.accounts import ProfileNotConfigured
from mezzanine.accounts.forms import ProfileForm
from mezzanine.accounts.forms import ProfileForm, PasswordResetForm
from mezzanine.conf import settings
from mezzanine.utils.tests import TestCase

Expand Down Expand Up @@ -79,3 +80,161 @@ def test_account(self):
self.assertEqual(response.status_code, 200)
users = User.objects.filter(email=data["email"], is_active=True)
self.assertEqual(len(users), 1)

self.client.logout()

if django.VERSION[0:1] >= (4, 1):
# This form of assertFormError is only available since Django 4.1

# Create another account with the same user name
settings.ACCOUNTS_VERIFICATION_REQUIRED = False
data = self.account_data("test1")
form = ProfileForm(data=data)
self.assertFormError(form, 'username', 'This username is already registered')

# Create another account with the same user name, but case is different
data['username'] = 'TEST1'
form = ProfileForm(data=data)
self.assertFormError(form, 'username', 'This username is already registered')

# Create another account with a different username, but same email
data['username'] = 'test3'
form = ProfileForm(data=data)
self.assertFormError(form, 'email', 'This email is already registered')

# Create another account with a different username, but same email with different case
data['email'] = '[email protected]'
form = ProfileForm(data=data)
self.assertFormError(form, 'email', 'This email is already registered')


def test_account_login(self):
"""
Test account login
"""
# Create test user account
data = self.account_data("test1")
settings.ACCOUNTS_VERIFICATION_REQUIRED = False
response = self.client.post(reverse("signup"), data, follow=True)
self.assertEqual(response.status_code, 200)
# Find the valid user
users = User.objects.filter(email=data["email"], is_active=True)
self.assertEqual(len(users), 1)
test_user = users[0]

self.client.logout()

# Log in with username/password
self.assertTrue(self.client.login(username=data['username'],
password=data['password1']))
user = get_user(self.client)
self.assertEqual(user, test_user)
self.assertTrue(user.is_authenticated)
self.client.logout()

# Log in with email/password
self.assertTrue(self.client.login(username=data['email'],
password=data['password1']))
user = get_user(self.client)
self.assertEqual(user, test_user)
self.assertTrue(user.is_authenticated)
self.client.logout()

# Log in with bad password
self.assertFalse(self.client.login(username=data['username'],
password=data['password1'] + 'badbit'))
user = get_user(self.client)
self.assertFalse(user.is_authenticated)
self.client.logout()

# Log in with username (different case) and password
self.assertTrue(self.client.login(username=data['username'].upper(),
password=data['password1']))
user = get_user(self.client)
self.assertEqual(user, test_user)
self.assertTrue(user.is_authenticated)
self.client.logout()

# Log in with email (different case) and password
self.assertTrue(self.client.login(username=data['email'].upper(),
password=data['password1']))
user = get_user(self.client)
self.assertEqual(user, test_user)
self.assertTrue(user.is_authenticated)
self.client.logout()

def _verify_password_reset_email(self, new_user, num_emails):
# Check email was sent
self.assertEqual(len(mail.outbox), num_emails + 1)
self.assertEqual(len(mail.outbox[0].to), 1)
self.assertEqual(mail.outbox[0].to[0], new_user.email)
verification_url = reverse(
"password_reset_verify",
kwargs={
"uidb36": int_to_base36(new_user.id),
"token": default_token_generator.make_token(new_user),
},
)
response = self.client.get(verification_url, follow=True)
self.assertEqual(response.status_code, 200)


def test_account_password_reset(self):
"""
Test account password reset verification email
"""
# Create test user account
data = self.account_data("test1")
settings.ACCOUNTS_VERIFICATION_REQUIRED = False
response = self.client.post(reverse("signup"), data, follow=True)
self.assertEqual(response.status_code, 200)
# Find the valid user
users = User.objects.filter(email=data["email"], is_active=True)
self.assertEqual(len(users), 1)
new_user = users[0]
self.client.logout()

# Reset password with username
emails = len(mail.outbox)
rdata = {'username': data['username']}
response = self.client.post(reverse("mezzanine_password_reset"), rdata, follow=True)
self.assertEqual(response.status_code, 200)
self._verify_password_reset_email(new_user, emails)
self.client.logout()

# Reset password with email
emails = len(mail.outbox)
rdata = {'username': data['email']}
response = self.client.post(reverse("mezzanine_password_reset"), rdata, follow=True)
self.assertEqual(response.status_code, 200)
self._verify_password_reset_email(new_user, emails)
self.client.logout()

# Reset password with username (different case)
emails = len(mail.outbox)
rdata = {'username': data['username'].upper()}
response = self.client.post(reverse("mezzanine_password_reset"), rdata, follow=True)
self.assertEqual(response.status_code, 200)
self._verify_password_reset_email(new_user, emails)
self.client.logout()

# Reset password with email (different case)
emails = len(mail.outbox)
rdata = {'username': data['email'].upper()}
response = self.client.post(reverse("mezzanine_password_reset"), rdata, follow=True)
self.assertEqual(response.status_code, 200)
self._verify_password_reset_email(new_user, emails)
self.client.logout()

if django.VERSION[0:1] >= (4, 1):
# This form of assertFormError is only available since Django 4.1

# Reset password with invalid username
rdata = {'username': 'badusername'}
form = PasswordResetForm(data=rdata)
self.assertFormError(form, None, 'Invalid username/email')

# Reset password with invalid email
rdata = {'username': '[email protected]'}
form = PasswordResetForm(data=rdata)
self.assertFormError(form, None, 'Invalid username/email')
Loading