From c6054066d575631ce1b4d5055aebb140e0a8835f Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 22 Jun 2017 11:24:42 +0200 Subject: [PATCH 1/2] Added password reset service. see #1151 --- modoboa/core/forms.py | 25 ++++ modoboa/core/tests/test_authentication.py | 39 +++++++ modoboa/core/views/__init__.py | 3 +- modoboa/core/views/auth.py | 16 ++- modoboa/static/css/login.css | 48 +------- modoboa/static/css/offline.css | 40 +++++++ modoboa/templates/registration/base.html | 42 +++++++ modoboa/templates/registration/login.html | 107 +++++++----------- .../registration/password_reset_complete.html | 12 ++ .../registration/password_reset_confirm.html | 23 ++++ .../registration/password_reset_done.html | 12 ++ .../registration/password_reset_email.html | 17 +++ .../registration/password_reset_form.html | 14 +++ .../registration/password_reset_subject.txt | 3 + modoboa/urls.py | 14 ++- 15 files changed, 299 insertions(+), 116 deletions(-) create mode 100644 modoboa/static/css/offline.css create mode 100644 modoboa/templates/registration/base.html create mode 100644 modoboa/templates/registration/password_reset_complete.html create mode 100644 modoboa/templates/registration/password_reset_confirm.html create mode 100644 modoboa/templates/registration/password_reset_done.html create mode 100644 modoboa/templates/registration/password_reset_email.html create mode 100644 modoboa/templates/registration/password_reset_form.html create mode 100644 modoboa/templates/registration/password_reset_subject.txt diff --git a/modoboa/core/forms.py b/modoboa/core/forms.py index c2a021f1f..733d2374e 100644 --- a/modoboa/core/forms.py +++ b/modoboa/core/forms.py @@ -5,8 +5,12 @@ from __future__ import unicode_literals from django import forms +from django.db.models import Q from django.utils.translation import ugettext as _, ugettext_lazy +from django.contrib.auth import forms as auth_forms +from django.contrib.auth import get_user_model + from passwords.fields import PasswordField from modoboa.core.models import User @@ -103,3 +107,24 @@ class APIAccessForm(forms.Form): user = kwargs.pop("user") super(APIAccessForm, self).__init__(*args, **kwargs) self.fields["enable_api_access"].initial = hasattr(user, "auth_token") + + +class PasswordResetForm(auth_forms.PasswordResetForm): + """Custom password reset form.""" + + def get_users(self, email): + """Return matching user(s) who should receive a reset.""" + return ( + get_user_model()._default_manager.filter( + email__iexact=email, is_active=True) + .exclude(Q(secondary_email__isnull=True) | Q(secondary_email="")) + ) + + def send_mail(self, subject_template_name, email_template_name, + context, from_email, to_email, + html_email_template_name=None): + """Send message to secondary email instead.""" + to_email = context["user"].secondary_email + super(PasswordResetForm, self).send_mail( + subject_template_name, email_template_name, + context, from_email, to_email, html_email_template_name) diff --git a/modoboa/core/tests/test_authentication.py b/modoboa/core/tests/test_authentication.py index 9e8e91230..07248bd73 100644 --- a/modoboa/core/tests/test_authentication.py +++ b/modoboa/core/tests/test_authentication.py @@ -7,6 +7,7 @@ import smtplib from unittest import skipIf from mock import patch +from django.core import mail from django.core.urlresolvers import reverse from django.test import override_settings @@ -45,6 +46,44 @@ class AuthenticationTestCase(ModoTestCase): self.assertTrue(response.url.endswith(reverse("core:dashboard"))) +class PasswordResetTestCase(ModoTestCase): + """Test password reset service.""" + + @classmethod + def setUpTestData(cls): + """Create test data.""" + super(PasswordResetTestCase, cls).setUpTestData() + cls.account_ok = factories.UserFactory( + username="user@test.com", secondary_email="test@ext.com", + groups=('SimpleUsers',) + ) + cls.account_ko = factories.UserFactory( + username="user2@test.com", groups=('SimpleUsers',) + ) + + def test_reset_password(self): + """Validate simple case.""" + self.client.logout() + url = reverse("password_reset") + data = {"email": self.account_ok.email} + response = self.client.post(url, data, follow=True) + self.assertContains( + response, + "We've emailed you instructions for setting your password") + self.assertEqual(len(mail.outbox), 1) + + def test_reset_password_no_secondary_email(self): + """Check email is not sent.""" + self.client.logout() + url = reverse("password_reset") + data = {"email": self.account_ko.email} + response = self.client.post(url, data, follow=True) + self.assertContains( + response, + "We've emailed you instructions for setting your password") + self.assertEqual(len(mail.outbox), 0) + + @skipIf(NO_SMTP, 'No SMTP server available') @override_settings(AUTHENTICATION_BACKENDS=( "modoboa.lib.authbackends.SMTPBackend", diff --git a/modoboa/core/views/__init__.py b/modoboa/core/views/__init__.py index 45177a4e3..f23a6b20a 100644 --- a/modoboa/core/views/__init__.py +++ b/modoboa/core/views/__init__.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals from .admin import ( check_top_notifications, information, logs, logs_page, parameters, viewsettings) -from .auth import dologin, dologout +from .auth import dologin, dologout, password_reset from .base import RootDispatchView from .dashboard import DashboardView from .user import api_access, index, preferences, profile @@ -22,6 +22,7 @@ __all__ = [ "logs", "logs_page", "parameters", + "password_reset", "preferences", "profile", "viewsettings", diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index b0d398530..681e1e96d 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -13,8 +13,9 @@ from django.utils.translation import ugettext as _ from django.views.decorators.cache import never_cache from django.contrib.auth import authenticate, login, logout +from django.contrib.auth import views as auth_views -from modoboa.core.forms import LoginForm +from modoboa.core import forms from .base import find_nextlocation from .. import signals @@ -26,7 +27,7 @@ def dologin(request): """Try to authenticate.""" error = None if request.method == "POST": - form = LoginForm(request.POST) + form = forms.LoginForm(request.POST) if form.is_valid(): logger = logging.getLogger('modoboa.auth') user = authenticate(username=form.cleaned_data["username"], @@ -59,7 +60,7 @@ def dologin(request): nextlocation = request.POST.get("next", None) httpcode = 401 else: - form = LoginForm() + form = forms.LoginForm() nextlocation = request.GET.get("next", None) httpcode = 200 @@ -87,3 +88,12 @@ def dologout(request): request.user.username)) logout(request) return HttpResponseRedirect(reverse("core:login")) + + +def password_reset(request, **kwargs): + """Custom view to override form.""" + kwargs.update({ + "from_email": "tonio@ngyn.org", + "password_reset_form": forms.PasswordResetForm + }) + return auth_views.password_reset(request, **kwargs) diff --git a/modoboa/static/css/login.css b/modoboa/static/css/login.css index f8e61d988..72be0abff 100644 --- a/modoboa/static/css/login.css +++ b/modoboa/static/css/login.css @@ -1,55 +1,13 @@ -@media (max-width: 767px) { - form.login-form div.checkbox button.btn{ - margin-top: 20px; - margin-bottom: 0; - } -} - -body{ - background-color: #EDEDEC; -} - -#logo{ - margin-bottom: 5%; -} - -.thumbnail{ - background-color: transparent; - border-color: transparent; -} -.form-actions { - background-color: transparent; - padding-right: 0px; - padding-left: 0px; - margin-bottom: 0px; -} - .login-error { margin: 20px auto; max-width: 300px; } -html body div.container div#box div#logo.row ul.thumbnails li{ - list-style-type: none; +form.login-form div.checkbox button.btn { + margin-bottom: 20px; } - -.thumbnails{ - padding-left: 0; -} - -.input-group { - border-collapse: separate; - display: table; - position: relative; -} - - -form.login-form div.checkbox button.btn{ - margin-bottom:20px; -} - -.login-form .form-group div.checkbox label{ +.login-form .form-group div.checkbox label { padding-top: 0; padding-bottom: 10px; } diff --git a/modoboa/static/css/offline.css b/modoboa/static/css/offline.css new file mode 100644 index 000000000..9defa70a5 --- /dev/null +++ b/modoboa/static/css/offline.css @@ -0,0 +1,40 @@ +@media (max-width: 767px) { + form div.checkbox button.btn { + margin-top: 20px; + margin-bottom: 0; + } +} + +body { + background-color: #EDEDEC; +} + +#logo { + margin-bottom: 5%; +} + +html body div.container div#box div#logo.row ul.thumbnails li { + list-style-type: none; +} + +.thumbnail { + background-color: transparent; + border-color: transparent; +} + +.thumbnails { + padding-left: 0; +} + +.input-group { + border-collapse: separate; + display: table; + position: relative; +} + +.form-actions { + background-color: transparent; + padding-right: 0px; + padding-left: 0px; + margin-bottom: 0px; +} \ No newline at end of file diff --git a/modoboa/templates/registration/base.html b/modoboa/templates/registration/base.html new file mode 100644 index 000000000..774264c14 --- /dev/null +++ b/modoboa/templates/registration/base.html @@ -0,0 +1,42 @@ + +{% load i18n core_tags static %} + + + + + + {% trans "Welcome to Modoboa" %} + + + + {% block extra_css %}{% endblock %} + + +
+
+ +
+ {% block main_content %}{% endblock %} +
+ {% block sub_content %}{% endblock %} +
+ + + + {% if TEMPLATE_DEBUG %} + + {% endif %} + + {% block extra_js %}{% endblock %} + + diff --git a/modoboa/templates/registration/login.html b/modoboa/templates/registration/login.html index d7c3f4263..21510855f 100644 --- a/modoboa/templates/registration/login.html +++ b/modoboa/templates/registration/login.html @@ -1,71 +1,46 @@ - -{% load i18n core_tags lib_tags form_tags %} -{% load static %} - - - - - - {% trans "Welcome to Modoboa" %} - - - - - -
-
- -
- -