mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge pull request #1152 from modoboa/feature/password_reset_service
Added password reset service.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user