0007-add-new-widget-and-fields-for-passwords-24439.patch
src/authentic2/app_settings.py | ||
---|---|---|
146 | 146 |
A2_PASSWORD_POLICY_CLASS=Setting( |
147 | 147 |
default='authentic2.passwords.DefaultPasswordChecker', |
148 | 148 |
definition='path of a class to validate passwords'), |
149 |
A2_PASSWORD_POLICY_SHOW_LAST_CHAR=Setting(default=False, definition='Show last character in password fields'), |
|
149 | 150 |
A2_AUTH_PASSWORD_ENABLE=Setting(default=True, definition='Activate login/password authentication', names=('AUTH_PASSWORD',)), |
150 | 151 |
A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting(default=0, |
151 | 152 |
definition='Failure count before logging a warning to ' |
src/authentic2/forms/fields.py | ||
---|---|---|
1 |
from django.forms import CharField |
|
2 |
from django.utils.translation import ugettext_lazy as _ |
|
3 | ||
4 |
from authentic2.passwords import password_help_text, validate_password |
|
5 |
from authentic2.forms.widgets import PasswordInput, NewPasswordInput, CheckPasswordInput |
|
6 | ||
7 | ||
8 |
class PasswordField(CharField): |
|
9 |
widget = PasswordInput |
|
10 | ||
11 | ||
12 |
class NewPasswordField(CharField): |
|
13 |
widget = NewPasswordInput |
|
14 |
default_validators = [validate_password] |
|
15 | ||
16 |
def __init__(self, *args, **kwargs): |
|
17 |
kwargs['help_text'] = password_help_text() |
|
18 |
super(NewPasswordField, self).__init__(*args, **kwargs) |
|
19 | ||
20 | ||
21 |
class CheckPasswordField(CharField): |
|
22 |
widget = CheckPasswordInput |
|
23 | ||
24 |
def __init__(self, *args, **kwargs): |
|
25 |
kwargs['help_text'] = u''' |
|
26 |
<span class="a2-password-check-equality-default">%(default)s</span> |
|
27 |
<span class="a2-password-check-equality-matched">%(match)s</span> |
|
28 |
<span class="a2-password-check-equality-unmatched">%(nomatch)s</span> |
|
29 |
''' % { |
|
30 |
'default': _('Both passwords must match.'), |
|
31 |
'match': _('Passwords match.'), |
|
32 |
'nomatch': _('Passwords do not match.'), |
|
33 |
} |
|
34 |
super(CheckPasswordField, self).__init__(*args, **kwargs) |
|
35 |
src/authentic2/forms/widgets.py | ||
---|---|---|
12 | 12 |
import uuid |
13 | 13 | |
14 | 14 |
from django.forms.widgets import DateTimeInput, DateInput, TimeInput |
15 |
from django.forms.widgets import PasswordInput as BasePasswordInput |
|
15 | 16 |
from django.utils.formats import get_language, get_format |
16 | 17 |
from django.utils.safestring import mark_safe |
17 | 18 |
from django.utils.translation import ugettext_lazy as _ |
18 | 19 | |
19 | 20 |
from gadjo.templatetags.gadjo import xstatic |
20 | 21 | |
22 |
from authentic2 import app_settings |
|
23 | ||
21 | 24 |
DATE_FORMAT_JS_PY_MAPPING = { |
22 | 25 |
'P': '%p', |
23 | 26 |
'ss': '%S', |
... | ... | |
197 | 200 |
options['format'] = options.get('format', self.get_format()) |
198 | 201 | |
199 | 202 |
super(TimeWidget, self).__init__(attrs, options, usel10n) |
203 | ||
204 | ||
205 |
class PasswordInput(BasePasswordInput): |
|
206 |
class Media: |
|
207 |
js = ('authentic2/js/password.js',) |
|
208 |
css = { |
|
209 |
'all': ('authentic2/css/password.css',) |
|
210 |
} |
|
211 | ||
212 |
def render(self, name, value, attrs=None): |
|
213 |
output = super(PasswordInput, self).render(name, value, attrs=attrs) |
|
214 |
if attrs and app_settings.A2_PASSWORD_POLICY_SHOW_LAST_CHAR: |
|
215 |
_id = attrs.get('id') |
|
216 |
if _id: |
|
217 |
output += u'''\n<script>a2_password_show_last_char(%s);</script>''' % json.dumps(_id) |
|
218 |
return output |
|
219 | ||
220 | ||
221 |
class NewPasswordInput(PasswordInput): |
|
222 |
def render(self, name, value, attrs=None): |
|
223 |
output = super(NewPasswordInput, self).render(name, value, attrs=attrs) |
|
224 |
if attrs: |
|
225 |
_id = attrs.get('id') |
|
226 |
if _id: |
|
227 |
output += u'''\n<script>a2_password_validate(%s);</script>''' % json.dumps(_id) |
|
228 |
return output |
|
229 | ||
230 | ||
231 |
class CheckPasswordInput(PasswordInput): |
|
232 |
# this widget must be named xxx2 and the other widget xxx1, it's a |
|
233 |
# convention, js code expect it. |
|
234 |
def render(self, name, value, attrs=None): |
|
235 |
output = super(CheckPasswordInput, self).render(name, value, attrs=attrs) |
|
236 |
if attrs: |
|
237 |
_id = attrs.get('id') |
|
238 |
if _id and _id.endswith('2'): |
|
239 |
other_id = _id[:-1] + '1' |
|
240 |
output += u'''\n<script>a2_password_check_equality(%s, %s)</script>''' % ( |
|
241 |
json.dumps(other_id), |
|
242 |
json.dumps(_id), |
|
243 |
) |
|
244 |
return output |
src/authentic2/passwords.py | ||
---|---|---|
7 | 7 |
from django.utils.translation import ugettext as _ |
8 | 8 |
from django.utils.module_loading import import_string |
9 | 9 |
from django.utils.functional import lazy |
10 |
from django.utils.safestring import mark_safe |
|
10 | 11 |
from django.core.exceptions import ValidationError |
11 | 12 | |
12 | 13 |
from . import app_settings |
... | ... | |
110 | 111 |
def validate_password(password): |
111 | 112 |
error = password_help_text(password, only_errors=True) |
112 | 113 |
if error: |
113 |
raise ValidationError(error)
|
|
114 |
raise ValidationError(mark_safe(error))
|
|
114 | 115 | |
115 | 116 | |
116 | 117 |
def password_help_text(password='', only_errors=False): |
117 | 118 |
password_checker = get_password_checker() |
118 | 119 |
criteria = [check.label for check in password_checker(password) if not (only_errors and check.result)] |
119 | 120 |
if criteria: |
120 |
return _('In order to create a secure password, please use at least: %s') % (', '.join(criteria)) |
|
121 |
html_criteria = [u'<span class="a2-password-policy-rule">%s</span>' % criter for criter in criteria] |
|
122 |
return _('In order to create a secure password, please use at least: ' |
|
123 |
'<span class="a2-password-policy-container">%s</span>') % (''.join(html_criteria)) |
|
121 | 124 |
else: |
122 | 125 |
return '' |
123 | 126 |
src/authentic2/static/authentic2/css/password.css | ||
---|---|---|
1 |
/* position span to show last char */ |
|
2 |
.a2-password-show-last-char { |
|
3 |
text-align: center; |
|
4 |
width: 20px; |
|
5 |
font-weight: bold; |
|
6 |
} |
|
7 | ||
8 |
.a2-password-show-last-char + input[type=password] { |
|
9 |
padding-left: 1.25rem; |
|
10 |
} |
|
11 | ||
12 |
.a2-password-nok { |
|
13 |
color: red; |
|
14 |
} |
|
15 | ||
16 |
.a2-password-ok { |
|
17 |
color: green; |
|
18 |
} |
|
19 | ||
20 |
.a2-password-icon { |
|
21 |
display: inline-block; |
|
22 |
width: calc(18em / 14); |
|
23 |
text-align: center; |
|
24 |
font-style: normal; |
|
25 |
padding-right: 1em; |
|
26 |
} |
|
27 | ||
28 |
/* default circle icon */ |
|
29 |
.a2-password-policy-rule { |
|
30 |
padding: 1rex; |
|
31 |
} |
|
32 |
.a2-password-policy-rule:after { |
|
33 |
font-family: FontAwesome; |
|
34 |
display: inline-block; |
|
35 |
width: 3ex; |
|
36 |
text-align: center; |
|
37 |
content: "\f00d"; /* cross icon */ |
|
38 |
opacity: 0; |
|
39 |
} |
|
40 | ||
41 |
.a2-password-nok.a2-password-policy-rule:after { |
|
42 |
content: "\f00d"; /* cross icon */ |
|
43 |
color: red; |
|
44 |
opacity: 1; |
|
45 |
} |
|
46 | ||
47 |
.a2-password-ok.a2-password-policy-rule:after { |
|
48 |
content: "\f00c"; /* ok icon */ |
|
49 |
color: green; |
|
50 |
opacity: 1; |
|
51 |
} |
|
52 | ||
53 |
/* Equality check */ |
|
54 | ||
55 |
.a2-password-nok .a2-password-check-equality-default, |
|
56 |
.a2-password-ok .a2-password-check-equality-default { |
|
57 |
display: none; |
|
58 |
} |
|
59 | ||
60 |
.a2-password-check-equality-matched, |
|
61 |
.a2-password-check-equality-unmatched { |
|
62 |
display: none; |
|
63 |
opacity: 0; |
|
64 |
transition: all 0.3s ease; |
|
65 |
} |
|
66 | ||
67 |
.a2-password-nok .a2-password-check-equality-unmatched, |
|
68 |
.a2-password-ok .a2-password-check-equality-matched { |
|
69 |
display: inline; |
|
70 |
opacity: 1; |
|
71 |
} |
|
72 | ||
73 |
.a2-password-check-equality-default:after, |
|
74 |
.a2-password-check-equality-unmatched:after, |
|
75 |
.a2-password-check-equality-matched:after { |
|
76 |
font-family: FontAwesome; |
|
77 |
width: 1rem; |
|
78 |
display: inline-block; |
|
79 |
} |
|
80 |
.a2-password-check-equality-default:after { |
|
81 |
content: "\f00d"; /* cross icon */ |
|
82 |
opacity: 0; |
|
83 |
} |
|
84 | ||
85 |
.a2-password-check-equality-unmatched:after { |
|
86 |
content: "\f00d"; /* cross icon */ |
|
87 |
} |
|
88 | ||
89 |
.a2-password-check-equality-matched:after { |
|
90 |
content: "\f00c"; /* ok icon */ |
|
91 |
} |
src/authentic2/static/authentic2/js/password.js | ||
---|---|---|
1 |
a2_password_check_equality = (function () { |
|
2 |
return function(id1, id2) { |
|
3 |
$(function () { |
|
4 |
function check_equality() { |
|
5 |
setTimeout(function () { |
|
6 |
var $help_text = $input2.parent().find('.helptext'); |
|
7 |
var password1 = $input1.val(); |
|
8 |
var password2 = $input2.val(); |
|
9 | ||
10 |
if (! password2) { |
|
11 |
$help_text.removeClass('a2-password-nok'); |
|
12 |
$help_text.removeClass('a2-password-ok'); |
|
13 |
} else { |
|
14 |
var equal = (password1 == password2); |
|
15 |
$help_text.toggleClass('a2-password-ok', equal); |
|
16 |
$help_text.toggleClass('a2-password-nok', ! equal); |
|
17 |
} |
|
18 |
}, 0); |
|
19 |
} |
|
20 |
var $input1 = $('#' + id1); |
|
21 |
var $input2 = $('#' + id2); |
|
22 |
$input1.on('change keydown keyup keypress paste', check_equality); |
|
23 |
$input2.on('change keydown keyup keypress paste', check_equality); |
|
24 |
}); |
|
25 |
} |
|
26 |
})(); |
|
27 | ||
28 |
a2_password_validate = (function () { |
|
29 |
function toggle_error($elt) { |
|
30 |
$elt.removeClass('a2-password-check-equality-ok'); |
|
31 |
$elt.addClass('a2-password-check-equality-error'); |
|
32 |
} |
|
33 |
function toggle_ok($elt) { |
|
34 |
$elt.removeClass('a2-password-check-equality-error'); |
|
35 |
$elt.addClass('a2-password-check-equality-ok'); |
|
36 |
} |
|
37 |
function get_validation($input) { |
|
38 |
var password = $input.val(); |
|
39 |
var $help_text = $input.parent().find('.helptext'); |
|
40 |
var $policyContainer = $help_text.find('.a2-password-policy-container'); |
|
41 |
$.ajax({ |
|
42 |
method: 'POST', |
|
43 |
url: '/api/validate-password/', |
|
44 |
data: JSON.stringify({'password': password}), |
|
45 |
dataType: 'json', |
|
46 |
contentType: 'application/json; charset=utf-8', |
|
47 |
success: function(data) { |
|
48 |
if (! data.result) { |
|
49 |
return; |
|
50 |
} |
|
51 | ||
52 |
$policyContainer.empty(); |
|
53 |
$policyContainer.removeClass('a2-password-ok a2-password-nok'); |
|
54 |
for (var i = 0; i < data.checks.length; i++) { |
|
55 |
var error = data.checks[i]; |
|
56 | ||
57 |
var $rule = $('<span class="a2-password-policy-rule"/>'); |
|
58 |
$rule.text(error.label) |
|
59 |
$rule.appendTo($policyContainer); |
|
60 |
$rule.toggleClass('a2-password-ok', error.result); |
|
61 |
$rule.toggleClass('a2-password-nok', ! error.result); |
|
62 |
} |
|
63 |
} |
|
64 |
}); |
|
65 |
} |
|
66 |
function validate_password(event) { |
|
67 |
var $input = $(event.target); |
|
68 |
setTimeout(function () { |
|
69 |
get_validation($input); |
|
70 |
}, 0); |
|
71 |
} |
|
72 |
return function (id) { |
|
73 |
var $input = $('#' + id); |
|
74 |
$input.on('keyup.a2-password-validate paste.a2-password-validate', validate_password); |
|
75 |
} |
|
76 |
})(); |
|
77 | ||
78 |
a2_password_show_last_char = (function () { |
|
79 |
function debounce(func, milliseconds) { |
|
80 |
var timer; |
|
81 | ||
82 |
return function() { |
|
83 |
window.clearTimeout(timer); |
|
84 |
timer = window.setTimeout(function() { |
|
85 |
func(); |
|
86 |
}, milliseconds); |
|
87 |
}; |
|
88 |
} |
|
89 |
return function(id) { |
|
90 |
var $input = $('#' + id); |
|
91 |
var last_char_id = id + '-last-char'; |
|
92 | ||
93 |
var $span = $('<span class="a2-password-show-last-char" id="' + last_char_id + '"/>'); |
|
94 | ||
95 |
function show_last_char(event) { |
|
96 |
if (event.keyCode == 32 || event.key === undefined || event.key == "" |
|
97 |
|| event.key == "Unidentified" || event.key.length > 1 || event.ctrlKey) { |
|
98 |
return; |
|
99 |
} |
|
100 |
// import input's layout to the span |
|
101 |
$span.css({ |
|
102 |
'position': 'absolute', |
|
103 |
'font-size': $input.css('font-size'), |
|
104 |
'font-family': $input.css('font-family'), |
|
105 |
'line-height': $input.css('line-height'), |
|
106 |
'padding-top': $input.css('padding-top'), |
|
107 |
'padding-bottom': $input.css('padding-bottom'), |
|
108 |
'margin-top': $input.css('margin-top'), |
|
109 |
'margin-bottom': $input.css('margin-bottom'), |
|
110 |
'border-top-width': $input.css('border-top-width'), |
|
111 |
'border-bottom-width': $input.css('border-bottom-width'), |
|
112 |
'border-style': 'hidden', |
|
113 |
'top': $input.position().top, |
|
114 |
'left': $input.position().left, |
|
115 |
}); |
|
116 |
var duration = 1000; |
|
117 |
var id = $input.attr('id'); |
|
118 |
var last_char_id = id + '-last-char'; |
|
119 |
$('#' + last_char_id) |
|
120 |
.text(event.key) |
|
121 |
.animate({'opacity': 1}, { |
|
122 |
duration: 50, |
|
123 |
queue: false, |
|
124 |
complete: function () { |
|
125 |
var $this = $(this); |
|
126 |
window.setTimeout( |
|
127 |
debounce(function () { |
|
128 |
$this.animate({'opacity': 0}, { |
|
129 |
duration: 50 |
|
130 |
}); |
|
131 |
}, duration), duration); |
|
132 |
} |
|
133 |
}); |
|
134 |
} |
|
135 |
console.log($input.position()); |
|
136 |
// place span absolutery in padding-left of the input |
|
137 |
$input.before($span); |
|
138 |
// $input.parent().css({'position': 'relative'}); |
|
139 |
$input.on('keypress.a2-password-show-last-char', show_last_char); |
|
140 |
} |
|
141 |
})(); |
|
0 |
- |