0001-create-assisted-password-input-widgets-24438.patch
src/authentic2/app_settings.py | ||
---|---|---|
143 | 143 |
A2_PASSWORD_POLICY_MIN_LENGTH=Setting(default=8, definition='Minimum number of characters in a password'), |
144 | 144 |
A2_PASSWORD_POLICY_REGEX=Setting(default=None, definition='Regular expression for validating passwords'), |
145 | 145 |
A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting(default=None, definition='Error message to show when the password do not validate the regular expression'), |
146 |
A2_PASSWORD_WIDGET_SHOW_ALL_BUTTON=Setting(default=False, definition='Show a button on BasePasswordInput for the user to see password input text'), |
|
146 | 147 |
A2_PASSWORD_POLICY_CLASS=Setting( |
147 | 148 |
default='authentic2.passwords.DefaultPasswordChecker', |
148 | 149 |
definition='path of a class to validate passwords'), |
src/authentic2/passwords.py | ||
---|---|---|
76 | 76 |
if self.min_length: |
77 | 77 |
yield self.Check( |
78 | 78 |
result=len(password) >= self.min_length, |
79 |
label=_('at least %s characters') % self.min_length)
|
|
79 |
label=_('%s characters') % self.min_length) |
|
80 | 80 | |
81 | 81 |
if self.at_least_one_lowercase: |
82 | 82 |
yield self.Check( |
83 | 83 |
result=any(c.islower() for c in password), |
84 |
label=_('at least 1 lowercase letter'))
|
|
84 |
label=_('1 lowercase letter')) |
|
85 | 85 | |
86 | 86 |
if self.at_least_one_digit: |
87 | 87 |
yield self.Check( |
88 | 88 |
result=any(c.isdigit() for c in password), |
89 |
label=_('at least 1 digit'))
|
|
89 |
label=_('1 digit')) |
|
90 | 90 | |
91 | 91 |
if self.at_least_one_uppercase: |
92 | 92 |
yield self.Check( |
93 | 93 |
result=any(c.isupper() for c in password), |
94 |
label=_('at least 1 uppercase letter'))
|
|
94 |
label=_('1 uppercase letter')) |
|
95 | 95 | |
96 | 96 |
if self.regexp and self.regexp_label: |
97 | 97 |
yield self.Check( |
src/authentic2/registration_backend/forms.py | ||
---|---|---|
1 | 1 |
import re |
2 | 2 |
import copy |
3 | 3 |
from collections import OrderedDict |
4 |
import json |
|
4 | 5 | |
5 | 6 |
from django.conf import settings |
6 | 7 |
from django.core.exceptions import ValidationError |
... | ... | |
15 | 16 |
from django.core.mail import send_mail |
16 | 17 |
from django.core import signing |
17 | 18 |
from django.template import RequestContext |
18 |
from django.template.loader import render_to_string |
|
19 | 19 |
from django.core.urlresolvers import reverse |
20 | 20 |
from django.core.validators import RegexValidator |
21 | 21 | |
22 |
from .widgets import CheckPasswordInput, NewPasswordInput |
|
22 | 23 |
from .. import app_settings, compat, forms, utils, validators, models, middleware, hooks |
23 | 24 |
from authentic2.a2_rbac.models import OrganizationalUnit |
24 | 25 | |
... | ... | |
115 | 116 | |
116 | 117 | |
117 | 118 |
class RegistrationCompletionForm(RegistrationCompletionFormNoPassword): |
118 |
password1 = CharField(widget=PasswordInput, label=_("Password"), |
|
119 |
validators=[validators.validate_password], |
|
120 |
help_text=validators.password_help_text()) |
|
121 |
password2 = CharField(widget=PasswordInput, label=_("Password (again)")) |
|
119 | ||
120 |
password1 = CharField(widget=NewPasswordInput(), label=_("Password"), |
|
121 |
validators=[validators.validate_password], |
|
122 |
help_text=validators.password_help_text()) |
|
123 |
password2 = CharField(widget=CheckPasswordInput(), label=_("Password (again)")) |
|
122 | 124 | |
123 | 125 |
def clean(self): |
124 | 126 |
""" |
src/authentic2/registration_backend/widgets.py | ||
---|---|---|
1 |
from django.forms import PasswordInput |
|
2 |
from django.template.loader import render_to_string |
|
3 |
from django.utils.encoding import force_text |
|
4 |
from django.utils.safestring import mark_safe |
|
5 | ||
6 |
from .. import app_settings |
|
7 | ||
8 | ||
9 |
class BasePasswordInput(PasswordInput): |
|
10 |
""" |
|
11 |
a password Input with some features to help the user choosing a new password |
|
12 |
Inspired by Django >= 1.11 new-style rendering |
|
13 |
(cf. https://docs.djangoproject.com/fr/1.11/ref/forms/renderers) |
|
14 |
""" |
|
15 |
template_name = 'authentic2/widgets/assisted_password.html' |
|
16 |
features = {} |
|
17 | ||
18 |
class Media: |
|
19 |
js = ('authentic2/js/password.js',) |
|
20 |
css = { |
|
21 |
'all': ('authentic2/css/password.css',) |
|
22 |
} |
|
23 | ||
24 |
def get_context(self, name, value, attrs): |
|
25 |
""" |
|
26 |
Base get_context |
|
27 |
""" |
|
28 |
context = { |
|
29 |
'app_settings': { |
|
30 |
'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, |
|
31 |
'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, |
|
32 |
'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX, |
|
33 |
}, |
|
34 |
'features': self.features |
|
35 |
} |
|
36 |
# attach data-* attributes for password.js to activate events |
|
37 |
attrs.update(dict([('data-%s' % feat.replace('_', '-'), is_active) for feat, is_active in self.features.items()])) |
|
38 | ||
39 |
context['widget'] = { |
|
40 |
'name': name, |
|
41 |
'is_hidden': self.is_hidden, |
|
42 |
'required': self.is_required, |
|
43 |
'template_name': self.template_name, |
|
44 |
'attrs': self.build_attrs(extra_attrs=attrs, name=name, type=self.input_type) |
|
45 |
} |
|
46 |
# Only add the 'value' attribute if a value is non-empty. |
|
47 |
if value is None: |
|
48 |
value = '' |
|
49 |
if value != '': |
|
50 |
context['widget']['value'] = force_text(self._format_value(value)) |
|
51 | ||
52 |
return context |
|
53 | ||
54 |
def render(self, name, value, attrs=None, **kwargs): |
|
55 |
""" |
|
56 |
Override render with a template-based system |
|
57 |
Remove this line when dropping Django 1.8, 1.9, 1.10 compatibility |
|
58 |
""" |
|
59 |
return mark_safe(render_to_string(self.template_name, |
|
60 |
self.get_context(name, value, attrs))) |
|
61 | ||
62 | ||
63 |
class CheckPasswordInput(BasePasswordInput): |
|
64 |
""" |
|
65 |
Password typing assistance widget (eg. password2) |
|
66 |
""" |
|
67 |
features = { |
|
68 |
'check_equality': True, |
|
69 |
'show_all': app_settings.A2_PASSWORD_WIDGET_SHOW_ALL_BUTTON, |
|
70 |
'show_last': True, |
|
71 |
} |
|
72 | ||
73 |
def get_context(self, name, value, attrs): |
|
74 |
context = super(CheckPasswordInput, self).get_context( |
|
75 |
name, value, attrs) |
|
76 |
return context |
|
77 | ||
78 | ||
79 |
class NewPasswordInput(CheckPasswordInput): |
|
80 |
""" |
|
81 |
Password creation assistance widget with policy (eg. password1) |
|
82 |
""" |
|
83 |
features = { |
|
84 |
'check_equality': False, |
|
85 |
'show_all': app_settings.A2_PASSWORD_WIDGET_SHOW_ALL_BUTTON, |
|
86 |
'show_last': True, |
|
87 |
'check_policy': True, |
|
88 |
} |
|
89 | ||
90 |
def get_context(self, name, value, attrs): |
|
91 |
context = super(NewPasswordInput, self).get_context(name, value, attrs) |
|
92 |
return context |
src/authentic2/static/authentic2/css/password.css | ||
---|---|---|
1 |
/* required in order to position a2-password-show-all and a2-password-show-last */ |
|
2 |
input[type=password].a2-password-assisted { |
|
3 |
padding-right: 60px; |
|
4 |
width: 100%; |
|
5 |
} |
|
6 | ||
7 |
.a2-password-icon { |
|
8 |
display: inline-block; |
|
9 |
width: calc(18em / 14); |
|
10 |
text-align: center; |
|
11 |
font-style: normal; |
|
12 |
padding-right: 1em; |
|
13 |
} |
|
14 | ||
15 |
/* default circle icon */ |
|
16 |
.a2-password-icon:before { |
|
17 |
font-family: FontAwesome; |
|
18 |
content: "\f111"; /* right hand icon */ |
|
19 |
font-size: 50%; |
|
20 |
} |
|
21 | ||
22 |
.a2-password-policy-helper { |
|
23 |
display: flex; |
|
24 |
height: auto; |
|
25 |
flex-direction: row; |
|
26 |
flex-wrap: wrap; |
|
27 |
position: relative; |
|
28 |
padding: 0.5rem 1rem; |
|
29 |
width: 90%; |
|
30 |
} |
|
31 | ||
32 |
/* we don't want helptext when a2-password-policy-helper is here */ |
|
33 |
.a2-password-policy-helper ~ .helptext { |
|
34 |
display: none; |
|
35 |
} |
|
36 | ||
37 |
.a2-password-policy-rule { |
|
38 |
flex: 1 1 50%; |
|
39 |
list-style: none; |
|
40 |
} |
|
41 | ||
42 |
.password-error { |
|
43 |
color: black; |
|
44 |
} |
|
45 | ||
46 |
.password-ok { |
|
47 |
color: green; |
|
48 |
} |
|
49 | ||
50 |
.password-error .a2-password-icon:before { |
|
51 |
content: "\f00d"; /* cross icon */ |
|
52 |
color: red; |
|
53 |
} |
|
54 | ||
55 |
.password-ok .a2-password-icon::before { |
|
56 |
content: "\f00c"; /* ok icon */ |
|
57 |
color: green; |
|
58 |
} |
|
59 | ||
60 |
.a2-password-show-last { |
|
61 |
position: relative; |
|
62 |
display: inline-block; |
|
63 |
float: right; |
|
64 |
opacity: 0; |
|
65 |
text-align: center; |
|
66 |
right: 10px; |
|
67 |
top: -4.5ex; |
|
68 |
width: 20px; |
|
69 |
} |
|
70 | ||
71 |
.a2-password-show-button { |
|
72 |
position: relative; |
|
73 |
display: inline-block; |
|
74 |
float: right; |
|
75 |
padding: 0; |
|
76 |
right: 10px; |
|
77 |
top: -4.4ex; |
|
78 |
cursor: pointer; |
|
79 |
width: 20px; |
|
80 |
} |
|
81 | ||
82 |
.a2-password-show-button:after { |
|
83 |
content: "\f06e"; /* eye */ |
|
84 |
font-family: FontAwesome; |
|
85 |
font-size: 125%; |
|
86 |
} |
|
87 | ||
88 |
.hide-password-button:after { |
|
89 |
content: "\f070"; /* crossed eye */ |
|
90 |
font-family: FontAwesome; |
|
91 |
font-size: 125%; |
|
92 |
} |
|
93 | ||
94 |
.a2-passwords-messages { |
|
95 |
display: block; |
|
96 |
padding: 0.5rem 1rem; |
|
97 |
} |
|
98 | ||
99 |
.a2-passwords-default { |
|
100 |
list-style: none; |
|
101 |
opacity: 0; |
|
102 |
} |
|
103 | ||
104 |
.password-error .a2-passwords-default, |
|
105 |
.password-ok .a2-passwords-default { |
|
106 |
display: none; |
|
107 |
} |
|
108 | ||
109 |
.a2-passwords-matched, |
|
110 |
.a2-passwords-unmatched { |
|
111 |
display: none; |
|
112 |
list-style: none; |
|
113 |
opacity: 0; |
|
114 |
transition: all 0.3s ease; |
|
115 |
} |
|
116 | ||
117 |
.password-error.a2-passwords-messages:before, |
|
118 |
.password-ok.a2-passwords-messages:before { |
|
119 |
display: none; |
|
120 |
} |
|
121 | ||
122 |
.password-error .a2-passwords-unmatched, |
|
123 |
.password-ok .a2-passwords-matched { |
|
124 |
display: block; |
|
125 |
opacity: 1; |
|
126 |
} |
|
127 | ||
128 |
.password-error .a2-passwords-unmatched .a2-password-icon:before { |
|
129 |
content: "\f00d"; /* cross icon */ |
|
130 |
color: red; |
|
131 |
} |
|
132 | ||
133 |
.password-ok .a2-passwords-matched .a2-password-icon:before { |
|
134 |
content: "\f00c"; /* ok icon */ |
|
135 |
color: green; |
|
136 |
} |
|
137 | ||
138 |
.a2-password-policy-intro { |
|
139 |
margin: 0; |
|
140 |
} |
src/authentic2/static/authentic2/css/style.css | ||
---|---|---|
76 | 76 |
.a2-log-message { |
77 | 77 |
white-space: pre-wrap; |
78 | 78 |
} |
79 | ||
80 |
.a2-registration-completion { |
|
81 |
padding: 1rem; |
|
82 |
min-width: 320px; |
|
83 |
width: 50%; |
|
84 |
} |
|
85 | ||
86 |
@media screen and (max-width: 800px) { |
|
87 |
.a2-registration-completion { |
|
88 |
width: 100%; |
|
89 |
} |
|
90 |
} |
|
91 | ||
92 |
.a2-registration-completion input, |
|
93 |
.a2-registration-completion select, |
|
94 |
.a2-registration-completion textarea |
|
95 |
{ |
|
96 |
width: 100%; |
|
97 |
} |
src/authentic2/static/authentic2/js/password.js | ||
---|---|---|
1 |
"use strict"; |
|
2 |
/* globals $, window, console */ |
|
3 | ||
4 |
$(function () { |
|
5 |
var debounce = function (func, milliseconds) { |
|
6 |
var timer; |
|
7 |
return function() { |
|
8 |
window.clearTimeout(timer); |
|
9 |
timer = window.setTimeout(function() { |
|
10 |
func(); |
|
11 |
}, milliseconds); |
|
12 |
}; |
|
13 |
} |
|
14 |
var toggleError = function($elt) { |
|
15 |
$elt.removeClass('password-ok'); |
|
16 |
$elt.addClass('password-error'); |
|
17 |
} |
|
18 |
var toggleOk = function($elt) { |
|
19 |
$elt.removeClass('password-error'); |
|
20 |
$elt.addClass('password-ok'); |
|
21 |
} |
|
22 |
/* |
|
23 |
* toggle error/ok on element with class names same as the validation code names |
|
24 |
* (cf. error_codes in authentic2.validators.validate_password) |
|
25 |
*/ |
|
26 |
var validatePassword = function(event) { |
|
27 |
var $this = $(event.target); |
|
28 |
if (!$this.val()) return; |
|
29 |
var password = $this.val(); |
|
30 |
var inputName = $this.attr('name'); |
|
31 |
getValidation(password, inputName); |
|
32 |
} |
|
33 |
var getValidation = function(password, inputName) { |
|
34 |
var policyContainer = $('#a2-password-policy-helper-' + inputName); |
|
35 |
$.ajax({ |
|
36 |
method: 'POST', |
|
37 |
url: '/api/validate-password/', |
|
38 |
data: JSON.stringify({'password': password}), |
|
39 |
dataType: 'json', |
|
40 |
contentType: 'application/json; charset=utf-8', |
|
41 |
success: function(data) { |
|
42 |
if (data.result) { |
|
43 |
policyContainer |
|
44 |
.empty() |
|
45 |
.removeClass('password-error password-ok'); |
|
46 |
data.checks.forEach(function (error) { |
|
47 |
var $li = $('<li class="a2-password-policy-rule"></li>') |
|
48 |
.html('<i class="a2-password-icon"></i>' + error.label) |
|
49 |
.appendTo(policyContainer); |
|
50 |
if (!error.result) { |
|
51 |
toggleError($li); |
|
52 |
} else { |
|
53 |
toggleOk($li); |
|
54 |
} |
|
55 |
}); |
|
56 |
} |
|
57 |
} |
|
58 |
}); |
|
59 |
} |
|
60 |
/* |
|
61 |
* Check password equality |
|
62 |
*/ |
|
63 |
var displayPasswordEquality = function($input, $inputTarget) { |
|
64 |
var messages = $('#a2-password-equality-helper-' + $input.attr('name')); |
|
65 |
var form = $input.parents('form'); |
|
66 |
if ($inputTarget === undefined) { |
|
67 |
$inputTarget = form.find('input[type=password]:not(input[name='+$input.attr('name')+'])'); |
|
68 |
} |
|
69 |
if (!$input.val() || !$inputTarget.val()) return; |
|
70 |
if ($inputTarget.val() !== $input.val()) { |
|
71 |
toggleError(messages); |
|
72 |
} else { |
|
73 |
toggleOk(messages); |
|
74 |
} |
|
75 |
} |
|
76 |
var passwordEquality = function () { |
|
77 |
var $this = $(this); |
|
78 |
displayPasswordEquality($this); |
|
79 |
} |
|
80 |
/* |
|
81 |
* Hide and show password handlers |
|
82 |
*/ |
|
83 |
var showPassword = function (event) { |
|
84 |
var $this = $(event.target); |
|
85 |
$this.addClass('hide-password-button'); |
|
86 |
var name = $this.attr('id').split('a2-password-show-button-')[1]; |
|
87 |
$('[name='+name+']').attr('type', 'text'); |
|
88 |
event.preventDefault(); |
|
89 |
} |
|
90 |
var hidePassword = function (event) { |
|
91 |
var $this = $(event.target); |
|
92 |
window.setTimeout(function () { |
|
93 |
$this.removeClass('hide-password-button'); |
|
94 |
var name = $this.attr('id').split('a2-password-show-button-')[1]; |
|
95 |
$('[name='+name+']').attr('type', 'password'); |
|
96 |
}, 3000); |
|
97 |
} |
|
98 |
/* |
|
99 |
* Show the last character |
|
100 |
*/ |
|
101 |
var showLastChar = function(event) { |
|
102 |
if (event.keyCode == 32 || event.key === undefined || event.key == "" |
|
103 |
|| event.key == "Unidentified" || event.key.length > 1) { |
|
104 |
return; |
|
105 |
} |
|
106 |
var duration = 1000; |
|
107 |
$('#a2-password-show-last-'+$(event.target).attr('name')) |
|
108 |
.text(event.key) |
|
109 |
.animate({'opacity': 1}, { |
|
110 |
duration: 50, |
|
111 |
queue: false, |
|
112 |
complete: function () { |
|
113 |
var $this = $(this); |
|
114 |
window.setTimeout( |
|
115 |
debounce(function () { |
|
116 |
$this.animate({'opacity': 0}, { |
|
117 |
duration: 50 |
|
118 |
}); |
|
119 |
}, duration), duration); |
|
120 |
} |
|
121 |
}); |
|
122 |
} |
|
123 |
/* |
|
124 |
* Init events |
|
125 |
*/ |
|
126 |
/* add password validation and equality check event handlers */ |
|
127 |
$('form input[type=password]:not(input[data-check-policy])').each(function () { |
|
128 |
$('#a2-password-policy-helper-' + $(this).attr('name')).hide(); |
|
129 |
}); |
|
130 |
$('body').on('keyup', 'form input[data-check-policy]', validatePassword); |
|
131 |
$('body').on('keyup', 'form input[data-check-equality]', passwordEquality); |
|
132 |
/* |
|
133 |
* Add event to handle displaying error/OK |
|
134 |
* while editing the first password |
|
135 |
* only if the second one is not empty |
|
136 |
*/ |
|
137 |
$('input[data-check-equality]') |
|
138 |
.each(function () { |
|
139 |
var $input2 = $(this); |
|
140 |
$('body') |
|
141 |
.on('keyup', 'form input[type=password]:not([name=' + $input2.attr('name') + '])', |
|
142 |
function (event) { |
|
143 |
var $input1 = $(event.target); |
|
144 |
if ($input2.val().length) { |
|
145 |
displayPasswordEquality($input2, $input1); |
|
146 |
} |
|
147 |
}); |
|
148 |
}); |
|
149 |
/* add the a2-password-show-button after the first input */ |
|
150 |
$('input[data-show-all]') |
|
151 |
.each(function () { |
|
152 |
var $this = $(this); |
|
153 |
if (!$('#a2-password-show-button-' + $this.attr('name')).length) { |
|
154 |
$(this).after($('<i class="a2-password-show-button" id="a2-password-show-button-' |
|
155 |
+ $this.attr('name') + '"></i>') |
|
156 |
.on('mousedown', showPassword) |
|
157 |
.on('mouseup mouseleave', hidePassword) |
|
158 |
); |
|
159 |
} |
|
160 |
}); |
|
161 |
/* show the last character on keypress */ |
|
162 |
$('input[data-show-last]') |
|
163 |
.each(function () { |
|
164 |
var $this = $(this); |
|
165 |
if (!$('#a2-password-show-last-' + $this.attr('name')).length) { |
|
166 |
// on crée un div placé dans le padding-right de l'input |
|
167 |
var $span = $('<span class="a2-password-show-last" id="a2-password-show-last-' |
|
168 |
+ $this.attr('name') + '"></span>)') |
|
169 |
$span.css({ |
|
170 |
'font-size': $this.css('font-size'), |
|
171 |
'font-family': $this.css('font-family'), |
|
172 |
'line-height': parseInt($this.css('line-height').replace('px', '')) - parseInt($this.css('padding-bottom').replace('px', '')) + 'px', |
|
173 |
'vertical-align': $this.css('vertical-align'), |
|
174 |
'padding-top': $this.css('padding-top'), |
|
175 |
'padding-bottom': $this.css('padding-bottom') |
|
176 |
}); |
|
177 |
$this.after($span); |
|
178 |
} |
|
179 |
}); |
|
180 |
$('body').on('keyup', 'form input[data-show-last]', showLastChar); |
|
181 |
}); |
src/authentic2/templates/authentic2/widgets/assisted_password.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
<input class="a2-password-assisted" {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "authentic2/widgets/attrs.html" %}> |
|
3 |
{% if features.check_policy %} |
|
4 |
<p class="a2-password-policy-intro">{% blocktrans %}In order to create a secure password, please use <i>at least</i> : {% endblocktrans %}</p> |
|
5 |
<ul class="a2-password-policy-helper" id="a2-password-policy-helper-{{ widget.attrs.name }}"> |
|
6 |
{% comment %}Required to display the initial rules on page load{% endcomment %} |
|
7 |
{% if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH %} |
|
8 |
<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% blocktrans with A2_PASSWORD_POLICY_MIN_LENGTH=app_settings.A2_PASSWORD_POLICY_MIN_LENGTH %}{{ A2_PASSWORD_POLICY_MIN_LENGTH }} characters{% endblocktrans %}</li> |
|
9 |
{% endif %} |
|
10 |
{% if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 0 %} |
|
11 |
<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% trans "1 lowercase letter" %}</li> |
|
12 |
{% endif %} |
|
13 |
{% if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 1 %} |
|
14 |
<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% trans "1 digit" %}</li> |
|
15 |
{% endif %} |
|
16 |
{% if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 2 %} |
|
17 |
<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% trans "1 uppercase letter" %}</li> |
|
18 |
{% endif %} |
|
19 |
{% if app_settings.A2_PASSWORD_POLICY_REGEX %} |
|
20 |
{% if app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG %} |
|
21 |
<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% blocktrans with A2_PASSWORD_POLICY_REGEX_ERROR_MSG=app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG %}{{ A2_PASSWORD_POLICY_REGEX_ERROR_MSG }}{% endblocktrans %}</li> |
|
22 |
{% else %} |
|
23 |
<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% blocktrans with A2_PASSWORD_POLICY_REGEX=app_settings.A2_PASSWORD_POLICY_REGEX %}Match the regular expression: {{ A2_PASSWORD_POLICY_REGEX }}, please change this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.'{% endblocktrans %}</li> |
|
24 |
{% endif %} |
|
25 |
{% endif %} |
|
26 |
</ul> |
|
27 |
{% endif %} |
|
28 |
{% if features.check_equality %} |
|
29 |
<ul class="a2-passwords-messages" id="a2-password-equality-helper-{{ widget.attrs.name }}"> |
|
30 |
<li class="a2-passwords-default"><i class="a2-password-icon"></i>{% trans 'Both passwords must match.' %}</li> |
|
31 |
<li class="a2-passwords-matched"><i class="a2-password-icon"></i>{% trans 'Passwords match.' %}</li> |
|
32 |
<li class="a2-passwords-unmatched"><i class="a2-password-icon"></i>{% trans 'Passwords do not match.' %}</li> |
|
33 |
</ul> |
|
34 |
{% endif %} |
src/authentic2/templates/authentic2/widgets/attrs.html | ||
---|---|---|
1 |
{% comment %}Will be deprecated in Django 1.11 : replace with django/forms/widgets/attrs.html{% endcomment %} |
|
2 |
{% for name, value in widget.attrs.items %}{% if value != False %} {{ name }}{% if value != True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %} |
src/authentic2/templates/registration/registration_completion_form.html | ||
---|---|---|
25 | 25 |
{% block content %} |
26 | 26 |
<h2>{% trans "Registration" %}</h2> |
27 | 27 |
<p>{% trans "Please fill the form to complete your registration" %}</p> |
28 |
<form method="post"> |
|
28 |
<form method="post" class="a2-registration-completion">
|
|
29 | 29 |
{% csrf_token %} |
30 | 30 |
{{ form.as_p }} |
31 | 31 |
<button class="submit-button">{% trans 'Submit' %}</button> |
tests/test_api.py | ||
---|---|---|
834 | 834 |
('x' * 8, False, True, True, False, False), |
835 | 835 |
('x' * 8 + '1', False, True, True, True, False), |
836 | 836 |
('x' * 8 + '1X', True, True, True, True, True)): |
837 |
response = app.post_json('/api/validate-password/', params={'password': password})
|
|
837 |
response = app.post_json('/api/validate-password/', params={'password': password}) |
|
838 | 838 |
assert response.json['result'] == 1 |
839 | 839 |
assert response.json['ok'] is ok |
840 | 840 |
assert len(response.json['checks']) == 4 |
841 |
assert response.json['checks'][0]['label'] == 'at least 8 characters'
|
|
841 |
assert response.json['checks'][0]['label'] == '8 characters' |
|
842 | 842 |
assert response.json['checks'][0]['result'] is length |
843 |
assert response.json['checks'][1]['label'] == 'at least 1 lowercase letter'
|
|
843 |
assert response.json['checks'][1]['label'] == '1 lowercase letter' |
|
844 | 844 |
assert response.json['checks'][1]['result'] is lower |
845 |
assert response.json['checks'][2]['label'] == 'at least 1 digit'
|
|
845 |
assert response.json['checks'][2]['label'] == '1 digit' |
|
846 | 846 |
assert response.json['checks'][2]['result'] is digit |
847 |
assert response.json['checks'][3]['label'] == 'at least 1 uppercase letter'
|
|
847 |
assert response.json['checks'][3]['label'] == '1 uppercase letter' |
|
848 | 848 |
assert response.json['checks'][3]['result'] is upper |
849 | 849 | |
850 | 850 | |
... | ... | |
856 | 856 |
assert response.json['result'] == 1 |
857 | 857 |
assert response.json['ok'] is False |
858 | 858 |
assert len(response.json['checks']) == 5 |
859 |
assert response.json['checks'][0]['label'] == 'at least 8 characters'
|
|
859 |
assert response.json['checks'][0]['label'] == '8 characters' |
|
860 | 860 |
assert response.json['checks'][0]['result'] is True |
861 |
assert response.json['checks'][1]['label'] == 'at least 1 lowercase letter'
|
|
861 |
assert response.json['checks'][1]['label'] == '1 lowercase letter' |
|
862 | 862 |
assert response.json['checks'][1]['result'] is True |
863 |
assert response.json['checks'][2]['label'] == 'at least 1 digit'
|
|
863 |
assert response.json['checks'][2]['label'] == '1 digit' |
|
864 | 864 |
assert response.json['checks'][2]['result'] is True |
865 |
assert response.json['checks'][3]['label'] == 'at least 1 uppercase letter'
|
|
865 |
assert response.json['checks'][3]['label'] == '1 uppercase letter' |
|
866 | 866 |
assert response.json['checks'][3]['result'] is True |
867 | 867 |
assert response.json['checks'][4]['label'] == 'must contain "ok"' |
868 | 868 |
assert response.json['checks'][4]['result'] is False |
... | ... | |
871 | 871 |
assert response.json['result'] == 1 |
872 | 872 |
assert response.json['ok'] is True |
873 | 873 |
assert len(response.json['checks']) == 5 |
874 |
assert response.json['checks'][0]['label'] == 'at least 8 characters'
|
|
874 |
assert response.json['checks'][0]['label'] == '8 characters' |
|
875 | 875 |
assert response.json['checks'][0]['result'] is True |
876 |
assert response.json['checks'][1]['label'] == 'at least 1 lowercase letter'
|
|
876 |
assert response.json['checks'][1]['label'] == '1 lowercase letter' |
|
877 | 877 |
assert response.json['checks'][1]['result'] is True |
878 |
assert response.json['checks'][2]['label'] == 'at least 1 digit'
|
|
878 |
assert response.json['checks'][2]['label'] == '1 digit' |
|
879 | 879 |
assert response.json['checks'][2]['result'] is True |
880 |
assert response.json['checks'][3]['label'] == 'at least 1 uppercase letter'
|
|
880 |
assert response.json['checks'][3]['label'] == '1 uppercase letter' |
|
881 | 881 |
assert response.json['checks'][3]['result'] is True |
882 | 882 |
assert response.json['checks'][4]['label'] == 'must contain "ok"' |
883 | 883 |
assert response.json['checks'][4]['result'] is True |
tests/test_registration.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 | |
3 |
import re |
|
3 | 4 |
from urlparse import urlparse |
4 | 5 | |
5 | 6 |
from django.core.urlresolvers import reverse |
... | ... | |
40 | 41 |
response.form.set('password1', 'toto') |
41 | 42 |
response.form.set('password2', 'toto') |
42 | 43 |
response = response.form.submit() |
43 |
assert 'at least 8 characters' in response.content
|
|
44 |
assert '8 characters' in response.content |
|
44 | 45 | |
45 | 46 |
# set valid password |
46 | 47 |
response.form.set('password1', 'T0==toto') |
... | ... | |
585 | 586 |
response = response.form.submit() |
586 | 587 |
assert new_next_url in response.content |
587 | 588 | |
589 | ||
590 |
def test_registration_activate_passwords_not_equal(app, db, settings, mailoutbox): |
|
591 |
settings.LANGUAGE_CODE = 'en-us' |
|
592 |
settings.A2_VALIDATE_EMAIL_DOMAIN = can_resolve_dns() |
|
593 |
settings.A2_EMAIL_IS_UNIQUE = True |
|
594 | ||
595 |
response = app.get(reverse('registration_register')) |
|
596 |
response.form.set('email', 'testbot@entrouvert.com') |
|
597 |
response = response.form.submit() |
|
598 |
response = response.follow() |
|
599 |
link = get_link_from_mail(mailoutbox[0]) |
|
600 |
response = app.get(link) |
|
601 |
response.form.set('password1', 'azerty12AZ') |
|
602 |
response.form.set('password2', 'AAAazerty12AZ') |
|
603 |
response = response.form.submit() |
|
604 |
assert "The two password fields didn't match." in response.content |
|
605 | ||
606 | ||
607 |
def test_registration_activate_assisted_password(app, db, settings, mailoutbox): |
|
608 |
response = app.get(reverse('registration_register')) |
|
609 |
response.form.set('email', 'testbot@entrouvert.com') |
|
610 |
response = response.form.submit() |
|
611 |
response = response.follow() |
|
612 |
link = get_link_from_mail(mailoutbox[0]) |
|
613 |
response = app.get(link) |
|
614 |
# check presence of the script and css for RegistrationCompletionForm to work |
|
615 |
assert "password.js" in response.content |
|
616 |
assert "password.css" in response.content |
|
617 |
# check default attributes for password.js and css to work |
|
618 |
assert re.search('<input class="a2-password-assisted".*data-show-last.*>', response.content, re.I | re.M | re.S) |
|
619 |
assert re.search('<input class="a2-password-assisted".*data-check-equality.*>', response.content, re.I | re.M | re.S) |
|
620 |
assert re.search('<input class="a2-password-assisted".*data-check-policy.*>', response.content, re.I | re.M | re.S) |
|
621 |
# check template containers for password.js to display its results |
|
622 |
assert re.search('class="a2-passwords-messages" id="a2-password-equality-helper-', response.content, re.I | re.M | re.S) |
|
623 |
assert re.search('class="a2-password-policy-helper" id="a2-password-policy-helper-', response.content, re.I | re.M | re.S) |
|
624 |
assert re.search('class="a2-password-policy-rule"', response.content, re.I | re.M | re.S) |
|
625 | ||
626 | ||
627 |
def test_registration_activate_password_no_show_all_button(app, db, settings, mailoutbox): |
|
628 |
response = app.get(reverse('registration_register')) |
|
629 |
response.form.set('email', 'testbot@entrouvert.com') |
|
630 |
response = response.form.submit() |
|
631 |
response = response.follow() |
|
632 |
link = get_link_from_mail(mailoutbox[0]) |
|
633 |
response = app.get(link) |
|
634 |
assert not re.search('<input class="a2-password-assisted".*data-show-all.*>', response.content, re.I | re.M | re.S) |
|
588 |
- |