From bafadeb366041a815e7216a3fbc2541063c4b041 Mon Sep 17 00:00:00 2001 From: Elias Showk Date: Thu, 14 Jun 2018 18:12:21 +0200 Subject: [PATCH] add password validation UI in registration view (#24439) --- src/authentic2/registration_backend/forms.py | 3 + src/authentic2/registration_backend/views.py | 6 ++ .../static/authentic2/css/style.css | 59 ++++++++++++ .../static/authentic2/js/password.js | 95 +++++++++++++++++++ .../registration_completion_form.html | 9 +- src/authentic2/validators.py | 40 ++++---- 6 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 src/authentic2/static/authentic2/js/password.js diff --git a/src/authentic2/registration_backend/forms.py b/src/authentic2/registration_backend/forms.py index 8ca7d9c5..2f0a6c3d 100644 --- a/src/authentic2/registration_backend/forms.py +++ b/src/authentic2/registration_backend/forms.py @@ -115,6 +115,9 @@ class RegistrationCompletionFormNoPassword(forms.BaseUserForm): class RegistrationCompletionForm(RegistrationCompletionFormNoPassword): + class Media: + js = ('authentic2/js/password.js',) + password1 = CharField(widget=PasswordInput, label=_("Password"), validators=[validators.validate_password], help_text=validators.password_help_text()) diff --git a/src/authentic2/registration_backend/views.py b/src/authentic2/registration_backend/views.py index 4f4e5715..19e04d0b 100644 --- a/src/authentic2/registration_backend/views.py +++ b/src/authentic2/registration_backend/views.py @@ -2,6 +2,7 @@ import django import collections import logging import random +import json from django.conf import settings from django.shortcuts import get_object_or_404 @@ -266,6 +267,11 @@ class RegistrationCompletionView(CreateView): ctx['email'] = self.email ctx['email_is_unique'] = self.email_is_unique ctx['create'] = 'create' in self.request.GET + ctx['passwordpolicy'] = json.dumps({ + 'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, + 'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, + 'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX, + }) return ctx def get(self, request, *args, **kwargs): diff --git a/src/authentic2/static/authentic2/css/style.css b/src/authentic2/static/authentic2/css/style.css index c7545cae..11ad9ced 100644 --- a/src/authentic2/static/authentic2/css/style.css +++ b/src/authentic2/static/authentic2/css/style.css @@ -76,3 +76,62 @@ .a2-log-message { white-space: pre-wrap; } + +.a2-min-class-policy, .a2-min-length-policy, .a2-regexp-policy { + display: inline; +} + +.password-error { + font-weight: bold; + color: red; +} + +.password-error:before { + content: "\f071"; + margin-right: 0.3em; + font-family: FontAwesome; + font-size: 100%; + color: red; +} + +.password-ok:before { + content: "\f00c"; + font-family: FontAwesome; + font-size: 100%; + color: green; +} + +.show-password-button { + padding-left: 0.5em; + cursor: pointer; +} + +.show-password-button:after { + content: "\f06e"; + font-family: FontAwesome; + font-size: 150%; +} + +.hide-password-button:after { + content: "\f070"; + font-family: FontAwesome; + font-size: 150%; +} + +.a2-passwords-unmatched { + display: none; + color: red; +} + +.a2-passwords-matched { + display: none; + color: green; +} + +.password-error .a2-passwords-unmatched { + display: inline; +} + +.password-ok .a2-passwords-matched { + display: inline; +} \ No newline at end of file diff --git a/src/authentic2/static/authentic2/js/password.js b/src/authentic2/static/authentic2/js/password.js new file mode 100644 index 00000000..37e6595f --- /dev/null +++ b/src/authentic2/static/authentic2/js/password.js @@ -0,0 +1,95 @@ +"use strict"; +/* globals $ */ + +$(function () { + var toggleError = function($elt) { + $elt.removeClass('password-ok'); + $elt.addClass('password-error'); + } + var toggleOk = function($elt) { + $elt.removeClass('password-error'); + $elt.addClass('password-ok'); + } + var validatePassword = function () { + var settings = $(this).data('passwordPolicy'); + var inputName = $(this).data('passwordPolicyInputName'); + var minClassElt = $(this).find('.a2-min-class-policy'); + var minLengthElt = $(this).find('.a2-min-length-policy'); + var regexpElt = $(this).find('.a2-regexp-policy'); + $(this) + .find('input[name='+inputName+']') + .each(function () { + var password = $(this).val(); + var min_len = settings.A2_PASSWORD_POLICY_MIN_LENGTH; + if (min_len && password.length < min_len) { + toggleError(minLengthElt); + } else { + toggleOk(minLengthElt); + } + var digits = /\d/g; + var lowerCase = /[a-z]/g; + var upperCase = /[A-Z]/g; + var punctuation = /'!"#\$%&\\'\(\)\*\+,-\.\/:;<=>\?@\[\]\^_`\{\|\}~'/g; + var minClassCount = settings.A2_PASSWORD_POLICY_MIN_CLASSES; + var classCount = 0; + if (minClassCount) { + [digits, lowerCase, upperCase, punctuation].forEach(function (cls) { + if (cls.test(password)) classCount++; + }) + if (classCount < minClassCount) { + toggleError(minClassElt); + } else { + toggleOk(minClassElt); + } + } + if (settings.A2_PASSWORD_POLICY_REGEX) { + var regExpObject = new RegExp(settings.A2_PASSWORD_POLICY_REGEX, 'g'); + if (!regExpObject.test(password)) { + toggleError(regexpElt); + } else { + toggleOk(regexpElt); + } + } + }); + } + var passwordEquality = function () { + var form = $(this).parents('form'); + var messages = form.find('.a2-password-messages'); + var input0 = form.find('input[type=password]').eq(0); + var input1 = form.find('input[type=password]').eq(1); + if (!input1.val() || !input0.val()) return; + if (input0.val() !== input1.val()) { + toggleError(messages); + } else { + toggleOk(messages); + } + } + var showPassword = function () { + $(this).addClass('hide-password-button'); + $(this) + .parents('form') + .find('input.showPassword') + .attr('type', 'text'); + } + var hidePassword = function () { + $(this).removeClass('hide-password-button'); + $(this) + .parents('form') + .find('input.showPassword') + .attr('type', 'password'); + } + + $('form[data-password-policy]') + .keyup(validatePassword); + $('form.passwordEquality input[type=password]') + .keyup(passwordEquality); + /* add the show-password-button after the first input */ + $('') + .insertAfter($('form.showPassword input[name='+ + $('form.showPassword').data('passwordShowInputName') + +']').addClass('showPassword')); + $('form.showPassword') + .on('mousedown', '.show-password-button', showPassword); + $('form.showPassword') + .on('mouseup', '.show-password-button', hidePassword); +}); diff --git a/src/authentic2/templates/registration/registration_completion_form.html b/src/authentic2/templates/registration/registration_completion_form.html index cd179aaa..46ef1dfd 100644 --- a/src/authentic2/templates/registration/registration_completion_form.html +++ b/src/authentic2/templates/registration/registration_completion_form.html @@ -25,9 +25,16 @@ {% block content %}

{% trans "Registration" %}

{% trans "Please fill the form to complete your registration" %}

-
+ {% csrf_token %} {{ form.as_p }} +
+ {% trans 'Passwords match.' %} + {% trans 'Passwords do not match.' %} +
{% endblock %} diff --git a/src/authentic2/validators.py b/src/authentic2/validators.py index c9e9b09b..ac24cb3d 100644 --- a/src/authentic2/validators.py +++ b/src/authentic2/validators.py @@ -120,26 +120,34 @@ class UsernameValidator(RegexValidator): def __password_help_text_helper(): - if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH and \ - app_settings.A2_PASSWORD_POLICY_MIN_CLASSES: - yield ugettext('Your password must contain at least %(min_length)d characters from at ' - 'least %(min_classes)d classes among: lowercase letters, uppercase letters, ' - 'digits and punctuations.') % { - 'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, - 'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES} + ''' + Password fields help_text + ''' + min_length_html = '%s' %\ + ugettext('Your password must contain at least %(min_length)d characters.' % + {'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH}) + + min_class_html = '%s' %\ + ugettext(('at least %(min_classes)d classes among: lowercase letters, uppercase letters, digits and punctuations.') % + {'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES}) + if app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG: + regexp_html = '%s' %\ + ugettext(app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG) + else: + regexp_html = '%s' %\ + ugettext('Your password must match the regular expression: %(regexp)s, please change this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.') % \ + {'regexp': app_settings.A2_PASSWORD_POLICY_REGEX} + + if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH and app_settings.A2_PASSWORD_POLICY_MIN_CLASSES: + yield '%s %s %s' % (min_length_html, _('from'), min_class_html) else: if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH: - yield ugettext('Your password must contain at least %(min_length)d characters.') % {'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH} + yield min_length_html if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES: - yield ugettext('Your password must contain characters from at least %(min_classes)d ' - 'classes among: lowercase letters, uppercase letters, digits ' - 'and punctuations.') % {'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES} + yield "%s %s" % (ugettext('Your password must contain characters from'), min_class_html) if app_settings.A2_PASSWORD_POLICY_REGEX: - yield ugettext(app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG) or \ - ugettext('Your password must match the regular expression: ' - '%(regexp)s, please change this message using the ' - 'A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.') % \ - {'regexp': app_settings.A2_PASSWORD_POLICY_REGEX} + yield regexp_html + def password_help_text(): return ' '.join(__password_help_text_helper()) -- 2.17.1