Merge pull request #1152 from modoboa/feature/password_reset_service

Added password reset service.
This commit is contained in:
Antoine Nguyen
2017-06-22 17:42:59 +02:00
committed by GitHub
15 changed files with 300 additions and 116 deletions
+25
View File
@@ -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 django.contrib.auth import password_validation
from modoboa.core.models import User
@@ -104,3 +108,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)
+39
View File
@@ -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",
+2 -1
View File
@@ -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",
+14 -3
View File
@@ -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,13 @@ 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": (
request.localconfig.parameters.get_value("sender_address")),
"password_reset_form": forms.PasswordResetForm
})
return auth_views.password_reset(request, **kwargs)
+3 -45
View File
@@ -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;
}
+40
View File
@@ -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;
}
+42
View File
@@ -0,0 +1,42 @@
<!DOCTYPE html>
{% load i18n core_tags static %}
<html lang="{{ LANGUAGE_CODE }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>{% trans "Welcome to Modoboa" %}</title>
<link rel="icon" type="image/png" href="{% static 'css/logo-icon.png' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/offline.css' %}" />
{% block extra_css %}{% endblock %}
</head>
<body>
<div class="container">
<div id="box" class="col-lg-9 col-lg-offset-1">
<div id="logo" class="row">
<ul class="thumbnails">
<li>
<div class="col-lg-offset-4 col-lg-5 col-md-offset-4 col-md-5 col-sm-offset-4 col-sm-5">
<a href="#" class="thumbnail">
<img src="{% get_modoboa_logo %}" />
</a>
</div>
</li>
</ul>
</div>
<div class=" divider"></div>
{% block main_content %}{% endblock %}
</div>
{% block sub_content %}{% endblock %}
</div>
<!-- Javascript -->
<script type="text/javascript" src="{% static 'jquery/jquery.js' %}"></script>
<script type="text/javascript" src="{% static 'bootstrap/dist/js/bootstrap.min.js' %}"></script>
{% if TEMPLATE_DEBUG %}
<script src="//maxcdn.bootstrapcdn.com/bootlint/0.4.0/bootlint.min.js"></script>
{% endif %}
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
+41 -66
View File
@@ -1,71 +1,46 @@
<!DOCTYPE html>
{% load i18n core_tags lib_tags form_tags %}
{% load static %}
<html lang="{{ LANGUAGE_CODE }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>{% trans "Welcome to Modoboa" %}</title>
<link rel="icon" type="image/png" href="{% static 'css/logo-icon.png' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/login.css' %}" />
</head>
<body>
<div class="container">
<div id="box" class="col-lg-9 col-lg-offset-1">
<div id="logo" class="row">
<ul class="thumbnails">
<li>
<div class="col-lg-offset-4 col-lg-5 col-md-offset-4 col-md-5 col-sm-offset-4 col-sm-5">
<a href="#" class="thumbnail">
<img src="{% get_modoboa_logo %}" />
</a>
</div>
</li>
</ul>
</div>
<div class=" divider"></div>
<form method="post" action="{% url 'core:login' %}" class="login-form form-horizontal">
{% csrf_token %}
{% render_field form.username %}
{% render_field form.password %}
<input type="hidden" name="next" value="{{ next }}" />
<div class="form-group">
<div class="control-label col-lg-4 col-md-4 col-sm-4 ">
</div>
<div class="col-lg-5 col-md-5 col-sm-5">
<div class="checkbox">
<label class="checkbox" for="id_rememberme">
<input type="checkbox" id="id_rememberme" name="rememberme" />{% trans "Remember me" %}
</label>
<button class="btn btn-primary col-lg-12" type="submit">{% trans "Log in" %}</button>
</div>
</div>
</div>
</form>
<div class="login-error">
{% if error %}{% alert error "danger" %}{% endif %}
{% extends "registration/base.html" %}
{% load i18n core_tags lib_tags form_tags static %}
{% block extra_css %}
<link rel="stylesheet" type="text/css" href="{% static 'css/login.css' %}" />
{% endblock %}
{% block main_content %}
<form method="post" action="{% url 'core:login' %}" class="login-form form-horizontal">
{% csrf_token %}
{% render_field form.username %}
{% render_field form.password %}
<input type="hidden" name="next" value="{{ next }}" />
<div class="form-group">
<div class="control-label col-lg-4 col-md-4 col-sm-4 ">
</div>
<div class="col-lg-5 col-md-5 col-sm-5">
<div class="checkbox">
<label class="checkbox" for="id_rememberme">
<input type="checkbox" id="id_rememberme" name="rememberme" />{% trans "Remember me" %}
</label>
<button class="btn btn-primary col-lg-12" type="submit">{% trans "Log in" %}</button>
<a class="pull-right" href="{% url 'password_reset' %}">{% trans "Forgot password?" %}</a>
</div>
</div>
{% for annoucement in annoucements %}
<div class="col-xs-12 col-sm-offset-3 col-sm-6">{{ annoucement|safe }}</div>
{% endfor %}
</div>
</form>
<div class="login-error">
{% if error %}{% alert error "danger" %}{% endif %}
</div>
{% endblock %}
<!-- Javascript -->
<script type="text/javascript" src="{% static 'jquery/jquery.js' %}"></script>
<script type="text/javascript" src="{% static 'bootstrap/dist/js/bootstrap.min.js' %}"></script>
{% if TEMPLATE_DEBUG %}
<script src="//maxcdn.bootstrapcdn.com/bootlint/0.4.0/bootlint.min.js"></script>
{% endif %}
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script type="text/javascript">
$(document).ready(function() {
$('#id_username').focus();
});
</script>
{% block sub_content %}
{% for annoucement in annoucements %}
<div class="col-xs-12 col-sm-offset-3 col-sm-6">{{ annoucement|safe }}</div>
{% endfor %}
{% endblock %}
</body>
</html>
{% block extra_js %}
<script type="text/javascript">
$(document).ready(function() {
$('#id_username').focus();
});
</script>
{% endblock %}
@@ -0,0 +1,12 @@
{% extends "registration/base.html" %}
{% load i18n %}
{% block main_content %}
<div class="alert alert-success">
{% url 'core:login' as login_url %}
{% blocktrans with url=login_url trimmed %}
Your password has been set. You may go ahead and <a href="{{ url }}">sign in</a> now.
{% endblocktrans %}
</div>
{% endblock %}
@@ -0,0 +1,23 @@
{% extends "registration/base.html" %}
{% load i18n form_tags %}
{% block main_content %}
{% if validlink %}
<h3 class="text-center">{% trans "Change password" %}</h3>
<form method="post" class="form-horizontal" style="margin-top: 20px">
{% csrf_token %}
{% render_form form %}
<div class="col-sm-offset-4 col-sm-5">
<button class="btn btn-primary col-lg-12" type="submit">{% trans "Submit" %}</button>
</div>
</form>
{% else %}
<div class="alert alert-danger">
{% blocktrans trimmed %}
The password reset link was invalid, possibly because it has already been used.
Please request a new password reset.
{% endblocktrans %}
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,12 @@
{% extends "registration/base.html" %}
{% load i18n %}
{% block main_content %}
<div class="alert alert-info">
{% blocktrans trimmed %}
<p>We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly.</p>
<p>If you don't receive an email, please make sure you've entered your primary address, and check your spam folder.</p>
{% endblocktrans %}
</div>
{% endblock %}
@@ -0,0 +1,17 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans with username=user.get_username trimmed %}
To initiate the password reset process for your {{ username }} Modoboa account, click the link below:
{% endblocktrans %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% blocktrans trimmed %}
If clicking the link above doesn't work, please copy and paste the URL in a new browser window instead.
{% endblocktrans %}
{% blocktrans trimmed %}
Sincerely, The Modoboa Team.
{% endblocktrans %}
{% endautoescape %}
@@ -0,0 +1,14 @@
{% extends "registration/base.html" %}
{% load i18n form_tags %}
{% block main_content %}
<h3 class="text-center">{% trans "Forgot password?" %}</h3>
<form class="form-horizontal" method="post" style="margin-top: 20px">
{% csrf_token %}
{% render_field form.email %}
<div class="col-sm-offset-4 col-sm-5">
<button type="submit" class="btn btn-primary col-lg-12">{% trans "Submit" %}</button>
</div>
</form>
{% endblock %}
@@ -0,0 +1,3 @@
{% load i18n %}
{% trans "Modoboa password reset" %}
+13 -1
View File
@@ -6,14 +6,16 @@ from django.conf import settings
from django.conf.urls import include, url
from django.views.i18n import JavaScriptCatalog
from django.contrib.auth import views as auth_views
from django.contrib.auth.decorators import login_required
from ckeditor_uploader import views as cku_views
from rest_framework.documentation import include_docs_urls
from modoboa.admin.views.user import forward
from modoboa.core.extensions import exts_pool
from modoboa.core import signals as core_signals
from modoboa.core import views as core_views
from modoboa.core.extensions import exts_pool
API_TITLE = 'Modoboa API'
@@ -28,6 +30,16 @@ urlpatterns = [
url('admin/', include('modoboa.admin.urls', namespace="admin")),
url('relaydomains/',
include('modoboa.relaydomains.urls', namespace="relaydomains")),
# No namespace
url(r'^accounts/password_reset/$', core_views.password_reset,
name="password_reset"),
url(r'^accounts/password_reset/done/$', auth_views.password_reset_done,
name="password_reset_done"),
url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', # noqa
auth_views.password_reset_confirm, name="password_reset_confirm"),
url(r'^reset/done/$', auth_views.password_reset_complete,
name="password_reset_complete"),
]
exts_pool.load_all()