add new widget and fields for passwords (#24439)
This commit is contained in:
parent
6a44c5f558
commit
f36b480419
|
@ -146,6 +146,7 @@ default_settings = dict(
|
|||
A2_PASSWORD_POLICY_CLASS=Setting(
|
||||
default='authentic2.passwords.DefaultPasswordChecker',
|
||||
definition='path of a class to validate passwords'),
|
||||
A2_PASSWORD_POLICY_SHOW_LAST_CHAR=Setting(default=False, definition='Show last character in password fields'),
|
||||
A2_AUTH_PASSWORD_ENABLE=Setting(default=True, definition='Activate login/password authentication', names=('AUTH_PASSWORD',)),
|
||||
A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting(default=0,
|
||||
definition='Failure count before logging a warning to '
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
from django.forms import CharField
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from authentic2.passwords import password_help_text, validate_password
|
||||
from authentic2.forms.widgets import PasswordInput, NewPasswordInput, CheckPasswordInput
|
||||
|
||||
|
||||
class PasswordField(CharField):
|
||||
widget = PasswordInput
|
||||
|
||||
|
||||
class NewPasswordField(CharField):
|
||||
widget = NewPasswordInput
|
||||
default_validators = [validate_password]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['help_text'] = password_help_text()
|
||||
super(NewPasswordField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class CheckPasswordField(CharField):
|
||||
widget = CheckPasswordInput
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['help_text'] = u'''
|
||||
<span class="a2-password-check-equality-default">%(default)s</span>
|
||||
<span class="a2-password-check-equality-matched">%(match)s</span>
|
||||
<span class="a2-password-check-equality-unmatched">%(nomatch)s</span>
|
||||
''' % {
|
||||
'default': _('Both passwords must match.'),
|
||||
'match': _('Passwords match.'),
|
||||
'nomatch': _('Passwords do not match.'),
|
||||
}
|
||||
super(CheckPasswordField, self).__init__(*args, **kwargs)
|
||||
|
|
@ -12,12 +12,15 @@ import re
|
|||
import uuid
|
||||
|
||||
from django.forms.widgets import DateTimeInput, DateInput, TimeInput
|
||||
from django.forms.widgets import PasswordInput as BasePasswordInput
|
||||
from django.utils.formats import get_language, get_format
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from gadjo.templatetags.gadjo import xstatic
|
||||
|
||||
from authentic2 import app_settings
|
||||
|
||||
DATE_FORMAT_JS_PY_MAPPING = {
|
||||
'P': '%p',
|
||||
'ss': '%S',
|
||||
|
@ -197,3 +200,45 @@ class TimeWidget(PickerWidgetMixin, TimeInput):
|
|||
options['format'] = options.get('format', self.get_format())
|
||||
|
||||
super(TimeWidget, self).__init__(attrs, options, usel10n)
|
||||
|
||||
|
||||
class PasswordInput(BasePasswordInput):
|
||||
class Media:
|
||||
js = ('authentic2/js/password.js',)
|
||||
css = {
|
||||
'all': ('authentic2/css/password.css',)
|
||||
}
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
output = super(PasswordInput, self).render(name, value, attrs=attrs)
|
||||
if attrs and app_settings.A2_PASSWORD_POLICY_SHOW_LAST_CHAR:
|
||||
_id = attrs.get('id')
|
||||
if _id:
|
||||
output += u'''\n<script>a2_password_show_last_char(%s);</script>''' % json.dumps(_id)
|
||||
return output
|
||||
|
||||
|
||||
class NewPasswordInput(PasswordInput):
|
||||
def render(self, name, value, attrs=None):
|
||||
output = super(NewPasswordInput, self).render(name, value, attrs=attrs)
|
||||
if attrs:
|
||||
_id = attrs.get('id')
|
||||
if _id:
|
||||
output += u'''\n<script>a2_password_validate(%s);</script>''' % json.dumps(_id)
|
||||
return output
|
||||
|
||||
|
||||
class CheckPasswordInput(PasswordInput):
|
||||
# this widget must be named xxx2 and the other widget xxx1, it's a
|
||||
# convention, js code expect it.
|
||||
def render(self, name, value, attrs=None):
|
||||
output = super(CheckPasswordInput, self).render(name, value, attrs=attrs)
|
||||
if attrs:
|
||||
_id = attrs.get('id')
|
||||
if _id and _id.endswith('2'):
|
||||
other_id = _id[:-1] + '1'
|
||||
output += u'''\n<script>a2_password_check_equality(%s, %s)</script>''' % (
|
||||
json.dumps(other_id),
|
||||
json.dumps(_id),
|
||||
)
|
||||
return output
|
||||
|
|
|
@ -7,6 +7,7 @@ import six
|
|||
from django.utils.translation import ugettext as _
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from . import app_settings
|
||||
|
@ -110,14 +111,16 @@ def get_password_checker(*args, **kwargs):
|
|||
def validate_password(password):
|
||||
error = password_help_text(password, only_errors=True)
|
||||
if error:
|
||||
raise ValidationError(error)
|
||||
raise ValidationError(mark_safe(error))
|
||||
|
||||
|
||||
def password_help_text(password='', only_errors=False):
|
||||
password_checker = get_password_checker()
|
||||
criteria = [check.label for check in password_checker(password) if not (only_errors and check.result)]
|
||||
if criteria:
|
||||
return _('In order to create a secure password, please use at least: %s') % (', '.join(criteria))
|
||||
html_criteria = [u'<span class="a2-password-policy-rule">%s</span>' % criter for criter in criteria]
|
||||
return _('In order to create a secure password, please use at least: '
|
||||
'<span class="a2-password-policy-container">%s</span>') % (''.join(html_criteria))
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/* position span to show last char */
|
||||
.a2-password-show-last-char {
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.a2-password-show-last-char + input[type=password] {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.a2-password-nok {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.a2-password-ok {
|
||||
color: green;
|
||||
}
|
||||
|
||||
/* default circle icon */
|
||||
.a2-password-policy-rule:after {
|
||||
font-family: FontAwesome;
|
||||
display: inline-block;
|
||||
width: 1.5ex;
|
||||
text-align: center;
|
||||
content: "\f00d"; /* cross icon */
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.a2-password-nok.a2-password-policy-rule:after {
|
||||
content: "\f00d"; /* cross icon */
|
||||
color: red;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.a2-password-ok.a2-password-policy-rule:after {
|
||||
content: "\f00c"; /* ok icon */
|
||||
color: green;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Equality check */
|
||||
|
||||
.a2-password-nok .a2-password-check-equality-default,
|
||||
.a2-password-ok .a2-password-check-equality-default {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.a2-password-check-equality-matched,
|
||||
.a2-password-check-equality-unmatched {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.a2-password-nok .a2-password-check-equality-unmatched,
|
||||
.a2-password-ok .a2-password-check-equality-matched {
|
||||
display: inline;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.a2-password-check-equality-default:after,
|
||||
.a2-password-check-equality-unmatched:after,
|
||||
.a2-password-check-equality-matched:after {
|
||||
font-family: FontAwesome;
|
||||
width: 1rem;
|
||||
display: inline-block;
|
||||
}
|
||||
.a2-password-check-equality-default:after {
|
||||
content: "\f00d"; /* cross icon */
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.a2-password-check-equality-unmatched:after {
|
||||
content: "\f00d"; /* cross icon */
|
||||
}
|
||||
|
||||
.a2-password-check-equality-matched:after {
|
||||
content: "\f00c"; /* ok icon */
|
||||
}
|
|
@ -76,3 +76,7 @@
|
|||
.a2-log-message {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.a2-password-policy-rule {
|
||||
padding: 0.3ex;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
a2_password_check_equality = (function () {
|
||||
return function(id1, id2) {
|
||||
$(function () {
|
||||
function check_equality() {
|
||||
setTimeout(function () {
|
||||
var $help_text = $input2.parent().find('.helptext');
|
||||
var password1 = $input1.val();
|
||||
var password2 = $input2.val();
|
||||
|
||||
if (! password2) {
|
||||
$help_text.removeClass('a2-password-nok');
|
||||
$help_text.removeClass('a2-password-ok');
|
||||
} else {
|
||||
var equal = (password1 == password2);
|
||||
$help_text.toggleClass('a2-password-ok', equal);
|
||||
$help_text.toggleClass('a2-password-nok', ! equal);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
var $input1 = $('#' + id1);
|
||||
var $input2 = $('#' + id2);
|
||||
$input1.on('change keydown keyup keypress paste', check_equality);
|
||||
$input2.on('change keydown keyup keypress paste', check_equality);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
a2_password_validate = (function () {
|
||||
function toggle_error($elt) {
|
||||
$elt.removeClass('a2-password-check-equality-ok');
|
||||
$elt.addClass('a2-password-check-equality-error');
|
||||
}
|
||||
function toggle_ok($elt) {
|
||||
$elt.removeClass('a2-password-check-equality-error');
|
||||
$elt.addClass('a2-password-check-equality-ok');
|
||||
}
|
||||
function get_validation($input) {
|
||||
var password = $input.val();
|
||||
var $help_text = $input.parent().find('.helptext');
|
||||
var $policyContainer = $help_text.find('.a2-password-policy-container');
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/api/validate-password/',
|
||||
data: JSON.stringify({'password': password}),
|
||||
dataType: 'json',
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
success: function(data) {
|
||||
if (! data.result) {
|
||||
return;
|
||||
}
|
||||
|
||||
$policyContainer.empty();
|
||||
$policyContainer.removeClass('a2-password-ok a2-password-nok');
|
||||
for (var i = 0; i < data.checks.length; i++) {
|
||||
var error = data.checks[i];
|
||||
|
||||
var $rule = $('<span class="a2-password-policy-rule"/>');
|
||||
$rule.text(error.label)
|
||||
$rule.appendTo($policyContainer);
|
||||
$rule.toggleClass('a2-password-ok', error.result);
|
||||
$rule.toggleClass('a2-password-nok', ! error.result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function validate_password(event) {
|
||||
var $input = $(event.target);
|
||||
setTimeout(function () {
|
||||
get_validation($input);
|
||||
}, 0);
|
||||
}
|
||||
return function (id) {
|
||||
var $input = $('#' + id);
|
||||
$input.on('keyup.a2-password-validate paste.a2-password-validate', validate_password);
|
||||
}
|
||||
})();
|
||||
|
||||
a2_password_show_last_char = (function () {
|
||||
function debounce(func, milliseconds) {
|
||||
var timer;
|
||||
|
||||
return function() {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(function() {
|
||||
func();
|
||||
}, milliseconds);
|
||||
};
|
||||
}
|
||||
return function(id) {
|
||||
var $input = $('#' + id);
|
||||
var last_char_id = id + '-last-char';
|
||||
|
||||
var $span = $('<span class="a2-password-show-last-char" id="' + last_char_id + '"/>');
|
||||
|
||||
function show_last_char(event) {
|
||||
if (event.keyCode == 32 || event.key === undefined || event.key == ""
|
||||
|| event.key == "Unidentified" || event.key.length > 1 || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
// import input's layout to the span
|
||||
$span.css({
|
||||
'position': 'absolute',
|
||||
'font-size': $input.css('font-size'),
|
||||
'font-family': $input.css('font-family'),
|
||||
'line-height': $input.css('line-height'),
|
||||
'padding-top': $input.css('padding-top'),
|
||||
'padding-bottom': $input.css('padding-bottom'),
|
||||
'margin-top': $input.css('margin-top'),
|
||||
'margin-bottom': $input.css('margin-bottom'),
|
||||
'border-top-width': $input.css('border-top-width'),
|
||||
'border-bottom-width': $input.css('border-bottom-width'),
|
||||
'border-style': 'hidden',
|
||||
'top': $input.position().top,
|
||||
'left': $input.position().left,
|
||||
});
|
||||
var duration = 1000;
|
||||
var id = $input.attr('id');
|
||||
var last_char_id = id + '-last-char';
|
||||
$('#' + last_char_id)
|
||||
.text(event.key)
|
||||
.animate({'opacity': 1}, {
|
||||
duration: 50,
|
||||
queue: false,
|
||||
complete: function () {
|
||||
var $this = $(this);
|
||||
window.setTimeout(
|
||||
debounce(function () {
|
||||
$this.animate({'opacity': 0}, {
|
||||
duration: 50
|
||||
});
|
||||
}, duration), duration);
|
||||
}
|
||||
});
|
||||
}
|
||||
// place span absolutery in padding-left of the input
|
||||
$input.before($span);
|
||||
$input.on('keypress.a2-password-show-last-char', show_last_char);
|
||||
}
|
||||
})();
|
Loading…
Reference in New Issue