diff --git a/.travis.yml b/.travis.yml index 993ca39..3f57807 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ language: python sudo: false +addons: + postgresql: "9.6" + python: - 3.4 - 3.5 @@ -9,6 +12,12 @@ python: env: - DJANGO_VERSION=1.11.16 - DJANGO_VERSION=2.0.9 + - DJANGO_VERSION=2.1.3 + +matrix: + exclude: + - python: 3.4 + env: DJANGO_VERSION=2.1.3 install: - pip install -q -r requirements/base.txt diff --git a/smartmin/__init__.py b/smartmin/__init__.py index c71ad57..35edfad 100644 --- a/smartmin/__init__.py +++ b/smartmin/__init__.py @@ -1,3 +1,3 @@ from __future__ import unicode_literals -__version__ = '2.0.2' +__version__ = '2.1.0' diff --git a/smartmin/backends.py b/smartmin/backends.py index 7e155b4..8135ec3 100644 --- a/smartmin/backends.py +++ b/smartmin/backends.py @@ -7,7 +7,7 @@ class CaseInsensitiveBackend(ModelBackend): Authenticates against settings.AUTH_USER_MODEL. """ - def authenticate(self, username=None, password=None, **kwargs): + def authenticate(self, request, username=None, password=None, **kwargs): User = get_user_model() try: user = User.objects.get(username__iexact=username) diff --git a/smartmin/users/urls.py b/smartmin/users/urls.py index 156304b..d4a4b5c 100644 --- a/smartmin/users/urls.py +++ b/smartmin/users/urls.py @@ -1,13 +1,19 @@ from django.conf import settings from django.conf.urls import url -from django.contrib.auth.views import logout -from .views import login, UserCRUDL +from django.contrib.auth.views import LogoutView +from .views import Login, UserCRUDL logout_url = getattr(settings, 'LOGOUT_REDIRECT_URL', None) urlpatterns = [ - url(r'^login/$', login, dict(template_name='smartmin/users/login.html'), name="users.user_login"), - url(r'^logout/$', logout, dict(redirect_field_name='go', next_page=logout_url), name="users.user_logout"), + url( + r'^login/$', Login.as_view(), dict(template_name='smartmin/users/login.html'), + name="users.user_login" + ), + url( + r'^logout/$', LogoutView.as_view(), dict(redirect_field_name='go', next_page=logout_url), + name="users.user_logout" + ), ] urlpatterns += UserCRUDL().as_urlpatterns() diff --git a/smartmin/users/views.py b/smartmin/users/views.py index acca0fb..890299c 100644 --- a/smartmin/users/views.py +++ b/smartmin/users/views.py @@ -5,10 +5,9 @@ from django import forms from django.conf import settings from django.contrib import messages, auth -from django.contrib.auth import get_user_model, REDIRECT_FIELD_NAME -from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from django.contrib.auth.views import login as django_login +from django.contrib.auth.views import LoginView from django.core.mail import send_mail from django.urls import reverse from django.http import HttpResponseRedirect @@ -358,7 +357,8 @@ def derive_success_message(self): def pre_process(self, request, *args, **kwargs): user = self.get_object() - login(request) + + Login.as_view()(request) # After logging in it is important to change the user stored in the session # otherwise the user will remain the same @@ -425,47 +425,52 @@ def get_context_data(self, *args, **kwargs): return context -def login(request, template_name='smartmin/users/login.html', - redirect_field_name=REDIRECT_FIELD_NAME, - authentication_form=AuthenticationForm, - current_app=None, extra_context=None): +class Login(LoginView): + template_name = 'smartmin/users/login.html' - lockout_timeout = getattr(settings, 'USER_LOCKOUT_TIMEOUT', 10) - failed_login_limit = getattr(settings, 'USER_FAILED_LOGIN_LIMIT', 5) - allow_email_recovery = getattr(settings, 'USER_ALLOW_EMAIL_RECOVERY', True) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) - if request.method == "POST": - if 'username' in request.POST and 'password' in request.POST: - # we are using AuthenticationForm in which username is CharField with strip=True that automatically strips - # whitespace characters, we need to copy that behaviour - username = request.POST['username'].strip() + context['allow_email_recovery'] = getattr(settings, 'USER_ALLOW_EMAIL_RECOVERY', True) - user = get_user_model().objects.filter(username__iexact=username).first() + return context - # this could be a valid login by a user - if user: + def post(self, request, *args, **kwargs): + form = self.get_form() + + # clean form data + form_is_valid = form.is_valid() + + lockout_timeout = getattr(settings, 'USER_LOCKOUT_TIMEOUT', 10) + failed_login_limit = getattr(settings, 'USER_FAILED_LOGIN_LIMIT', 5) - # incorrect password? create a failed login token - valid_password = user.check_password(request.POST['password']) - if not valid_password: - FailedLogin.objects.create(user=user) + user = get_user_model().objects.filter(username__iexact=form.cleaned_data.get('username')).first() - bad_interval = timezone.now() - timedelta(minutes=lockout_timeout) - failures = FailedLogin.objects.filter(user=user) + # this could be a valid login by a user + if user: - # if the failures reset after a period of time, then limit our query to that interval - if lockout_timeout > 0: - failures = failures.filter(failed_on__gt=bad_interval) + # incorrect password? create a failed login token + valid_password = user.check_password(form.cleaned_data.get('password')) + if not valid_password: + FailedLogin.objects.create(user=user) - # if there are too many failed logins, take them to the failed page - if len(failures) >= failed_login_limit: - return HttpResponseRedirect(reverse('users.user_failed')) + bad_interval = timezone.now() - timedelta(minutes=lockout_timeout) + failures = FailedLogin.objects.filter(user=user) - # delete failed logins if the password is valid - elif valid_password: - FailedLogin.objects.filter(user=user).delete() + # if the failures reset after a period of time, then limit our query to that interval + if lockout_timeout > 0: + failures = failures.filter(failed_on__gt=bad_interval) + + # if there are too many failed logins, take them to the failed page + if len(failures) >= failed_login_limit: + return HttpResponseRedirect(reverse('users.user_failed')) + + # delete failed logins if the password is valid + elif valid_password: + FailedLogin.objects.filter(user=user).delete() - return django_login(request, template_name='smartmin/users/login.html', - redirect_field_name=REDIRECT_FIELD_NAME, - authentication_form=AuthenticationForm, - extra_context=dict(allow_email_recovery=allow_email_recovery)) + # pass through the normal login process + if form_is_valid: + return self.form_valid(form) + else: + return self.form_invalid(form)