Projet

Général

Profil

0007-add-new-widget-and-fields-for-passwords-24439.patch

Benjamin Dauvergne, 20 juillet 2018 15:48

Télécharger (14,6 ko)

Voir les différences:

Subject: [PATCH 7/9] add new widget and fields for passwords (#24439)

 src/authentic2/app_settings.py                |   1 +
 src/authentic2/forms/fields.py                |  35 +++++
 src/authentic2/forms/widgets.py               |  45 ++++++
 src/authentic2/passwords.py                   |   7 +-
 .../static/authentic2/css/password.css        |  91 +++++++++++
 .../static/authentic2/js/password.js          | 141 ++++++++++++++++++
 6 files changed, 318 insertions(+), 2 deletions(-)
 create mode 100644 src/authentic2/forms/fields.py
 create mode 100644 src/authentic2/static/authentic2/css/password.css
 create mode 100644 src/authentic2/static/authentic2/js/password.js
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
-