From 4843fb1f2a7ef852b2e00dc937e637efd3dfa242 Mon Sep 17 00:00:00 2001 From: Elias Showk Date: Fri, 22 Jun 2018 18:26:15 +0200 Subject: [PATCH] create assisted password input widgets (#24438) --- src/authentic2/app_settings.py | 1 + .../locale/fr/LC_MESSAGES/django.po | 206 +++++++++++------- src/authentic2/passwords.py | 8 +- src/authentic2/registration_backend/forms.py | 12 +- .../registration_backend/widgets.py | 92 ++++++++ .../static/authentic2/css/password.css | 140 ++++++++++++ .../static/authentic2/css/style.css | 19 ++ .../static/authentic2/js/password.js | 181 +++++++++++++++ .../authentic2/widgets/assisted_password.html | 34 +++ .../templates/authentic2/widgets/attrs.html | 2 + .../registration_completion_form.html | 2 +- tests/test_api.py | 2 +- tests/test_registration.py | 47 ++++ 13 files changed, 655 insertions(+), 91 deletions(-) create mode 100644 src/authentic2/registration_backend/widgets.py create mode 100644 src/authentic2/static/authentic2/css/password.css create mode 100644 src/authentic2/static/authentic2/js/password.js create mode 100644 src/authentic2/templates/authentic2/widgets/assisted_password.html create mode 100644 src/authentic2/templates/authentic2/widgets/attrs.html diff --git a/src/authentic2/app_settings.py b/src/authentic2/app_settings.py index 015ea499..87f62772 100644 --- a/src/authentic2/app_settings.py +++ b/src/authentic2/app_settings.py @@ -143,6 +143,7 @@ default_settings = dict( A2_PASSWORD_POLICY_MIN_LENGTH=Setting(default=8, definition='Minimum number of characters in a password'), A2_PASSWORD_POLICY_REGEX=Setting(default=None, definition='Regular expression for validating passwords'), A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting(default=None, definition='Error message to show when the password do not validate the regular expression'), + A2_PASSWORD_WIDGET_SHOW_ALL_BUTTON=Setting(default=False, definition='Show a button on BasePasswordInput for the user to see password input text'), A2_PASSWORD_POLICY_CLASS=Setting( default='authentic2.passwords.DefaultPasswordChecker', definition='path of a class to validate passwords'), diff --git a/src/authentic2/locale/fr/LC_MESSAGES/django.po b/src/authentic2/locale/fr/LC_MESSAGES/django.po index 59aabf1d..10bedf97 100644 --- a/src/authentic2/locale/fr/LC_MESSAGES/django.po +++ b/src/authentic2/locale/fr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Authentic\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-05 16:39+0200\n" +"POT-Creation-Date: 2018-07-06 16:53+0200\n" "PO-Revision-Date: 2018-07-05 14:08+0200\n" "Last-Translator: Mikaël Ates \n" "Language-Team: None\n" @@ -17,7 +17,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n>1;\n" -#: authentic2/admin.py:26 +#: debian-jessie/multitenant/debian_config.py:37 +#: debian-wheezy/multitenant/debian_config.py:38 +#: authentic2/profile_forms.py:19 +#: authentic2/registration_backend/forms.py:33 +#: authentic2/templates/authentic2/api_user_create_registration_email_body.txt:4 +msgid "Email" +msgstr "Courriel" msgid "Cleanup expired objects" msgstr "Nettoyer les objets qui ont expiré" @@ -63,11 +69,11 @@ msgstr "domaine" msgid "You must at least give a username or an email to your user" msgstr "Un utilisateur doit au minimum posséder un courriel ou un identifiant." -#: authentic2/admin.py:167 authentic2/admin.py:204 -#: authentic2/auth_frontends.py:14 authentic2/forms.py:24 -#: authentic2/registration_backend/forms.py:118 -#: authentic2/registration_backend/forms.py:204 -#: authentic2/templates/authentic2/login_password_profile.html:4 +#: admin.py:167 admin.py:204 +#: auth_frontends.py:14 forms.py:24 +#: registration_backend/forms.py:120 +#: registration_backend/forms.py:206 +#: templates/authentic2/login_password_profile.html:4 msgid "Password" msgstr "Mot de passe" @@ -82,7 +88,7 @@ msgstr "" "modifier le mot de passe de l'usager en utilisant ce " "formulaire." -#: authentic2/admin.py:201 authentic2/registration_backend/forms.py:132 +#: admin.py:201 registration_backend/forms.py:134 msgid "The two password fields didn't match." msgstr "Les deux champs mot de passe ne correspondent pas." @@ -114,19 +120,19 @@ msgstr "Attributs" msgid "you are not authorized to create users in this ou" msgstr "Vous n'êtes pas autorisé à vous inscrire dans cette collectivité." -#: authentic2/api_views.py:119 authentic2/api_views.py:126 +#: authentic2/api_views.py:120 authentic2/api_views.py:127 msgid "You already have an account" msgstr "Vous avez déjà un compte." -#: authentic2/api_views.py:123 +#: authentic2/api_views.py:124 tests/test_all.py:530 tests/test_all.py:675 msgid "Username is required in this ou" msgstr "L'identifiant est requis dans cette collectivité." -#: authentic2/api_views.py:654 +#: authentic2/api_views.py:655 msgid "User successfully added to role" msgstr "Utilisateur ajouté au rôle" -#: authentic2/api_views.py:659 +#: authentic2/api_views.py:660 msgid "User successfully removed from role" msgstr "Utilisateur retiré du rôle" @@ -596,10 +602,25 @@ msgstr "application" msgid "base service models" msgstr "applications" -#: authentic2/profile_forms.py:19 authentic2/registration_backend/forms.py:32 -#: authentic2/templates/authentic2/api_user_create_registration_email_body.txt:4 -msgid "Email" -msgstr "Courriel" +#: authentic2/passwords.py:80 +#, python-format +msgid "%s characters" +msgstr "%s caractères" + +#: authentic2/passwords.py:85 +#: authentic2/templates/authentic2/widgets/assisted_password.html:12 +msgid "1 lowercase letter" +msgstr "1 minuscule" + +#: authentic2/passwords.py:90 +#: authentic2/templates/authentic2/widgets/assisted_password.html:15 +msgid "1 digit" +msgstr "1 chiffre" + +#: authentic2/passwords.py:95 +#: authentic2/templates/authentic2/widgets/assisted_password.html:18 +msgid "1 uppercase letter" +msgstr "1 majuscule" #: authentic2/profile_urls.py:41 #: authentic2/templates/registration/password_change_done.html:9 @@ -646,17 +667,17 @@ msgstr "" msgid "Enter new password" msgstr "Entrez un nouveau mot de passe" -#: authentic2/registration_backend/forms.py:52 +#: authentic2/registration_backend/forms.py:53 msgid "You cannot register with this email." msgstr "Vous ne pouvez pas vous inscrire avec cette adresse de courriel." -#: authentic2/registration_backend/forms.py:81 +#: authentic2/registration_backend/forms.py:82 tests/test_all.py:763 msgid "This username is already in use. Please supply a different username." msgstr "" "Cet identifiant est déjà utilisé. Utilisez s'il vous plait un autre " "identifiant." -#: authentic2/registration_backend/forms.py:100 +#: authentic2/registration_backend/forms.py:101 msgid "" "This email address is already in use. Please supply a different email " "address." @@ -664,49 +685,49 @@ msgstr "" "Cette adresse de courriel est déjà utilisée. Utilisez s'il vous plait une " "autre adresse de courriel." -#: authentic2/registration_backend/forms.py:121 +#: authentic2/registration_backend/forms.py:123 msgid "Password (again)" msgstr "Confirmation du mot de passe" -#: authentic2/registration_backend/forms.py:168 -#: authentic2/registration_backend/forms.py:182 +#: authentic2/registration_backend/forms.py:170 +#: authentic2/registration_backend/forms.py:184 msgid "New password" msgstr "Nouveau mot de passe" -#: authentic2/registration_backend/forms.py:176 -#: authentic2/registration_backend/forms.py:191 +#: authentic2/registration_backend/forms.py:178 +#: authentic2/registration_backend/forms.py:193 msgid "New password must differ from old password" msgstr "Le nouveau mot de passe doit être différent de l'ancien." -#: authentic2/registration_backend/forms.py:213 +#: authentic2/registration_backend/forms.py:215 msgid "Password is invalid" msgstr "Le mot de passe est invalide" -#: authentic2/registration_backend/views.py:44 +#: authentic2/registration_backend/views.py:45 msgid "Your activation key is expired" msgstr "Votre clé d'activation a expiré" -#: authentic2/registration_backend/views.py:47 +#: authentic2/registration_backend/views.py:48 msgid "Activation failed" msgstr "Échec à l'activation du compte" -#: authentic2/registration_backend/views.py:56 +#: authentic2/registration_backend/views.py:57 #: authentic2/templates/registration/registration_completion_choose.html:6 #: authentic2/templates/registration/registration_completion_form.html:17 #: authentic2/templates/registration/registration_completion_form.html:26 msgid "Registration" msgstr "Création d'un compte" -#: authentic2/registration_backend/views.py:344 +#: authentic2/registration_backend/views.py:345 msgid "You have just created an account." msgstr "Vous venez de créer un compte." -#: authentic2/registration_backend/views.py:369 +#: authentic2/registration_backend/views.py:370 #: authentic2/templates/authentic2/accounts.html:41 msgid "Delete account" msgstr "Supprimer votre compte" -#: authentic2/registration_backend/views.py:400 +#: authentic2/registration_backend/views.py:401 msgid "" "Your account has been scheduled for deletion. You cannot use it anymore." msgstr "" @@ -1133,7 +1154,44 @@ msgstr "" msgid "Notification: %(user)s, your account has been deleted" msgstr "Notification: %(user)s, votre compte a été supprimé" -#: authentic2/templates/error_ssl.html:4 +#: templates/authentic2/widgets/assisted_password.html:4 +msgid "In order to create a secure password, please use at least : " +msgstr "" +"Pour avoir un mot de passe sécurisé, veuillez utiliser à minima :" + +#: authentic2/templates/authentic2/widgets/assisted_password.html:9 +#, python-format +msgid "%(A2_PASSWORD_POLICY_MIN_LENGTH)s characters" +msgstr "%(A2_PASSWORD_POLICY_MIN_LENGTH)s caratères" + +#: templates/authentic2/widgets/assisted_password.html:16 +#, python-format +msgid "%(A2_PASSWORD_POLICY_REGEX_ERROR_MSG)s" +msgstr "%(A2_PASSWORD_POLICY_REGEX_ERROR_MSG)s" + +#: authentic2/templates/authentic2/widgets/assisted_password.html:24 +#, python-format +msgid "" +"Match the regular expression: %(A2_PASSWORD_POLICY_REGEX)s, please change " +"this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.'" +msgstr "" +"Valider l'expression régulière %(A2_PASSWORD_POLICY_REGEX)s. Veuillez " +"changer ce message en modifiant le paramètre " +"A2_PASSWORD_POLICY_REGEX_ERROR_MSG." + +#: authentic2/templates/authentic2/widgets/assisted_password.html:31 +msgid "Both passwords must match." +msgstr "Les deux mots de passe doivent être identiques." + +#: authentic2/templates/authentic2/widgets/assisted_password.html:32 +msgid "Passwords match." +msgstr "Les mots de passe sont identiques." + +#: authentic2/templates/authentic2/widgets/assisted_password.html:33 +msgid "Passwords do not match." +msgstr "Les deux mots de passe ne sont pas identiques." + +#: templates/error_ssl.html:4 msgid "Error: authentication failure" msgstr "Erreur: échec de l'authentification" @@ -1488,57 +1546,12 @@ msgstr "Adresse de courriel inexistante." #: authentic2/validators.py:95 #, python-format -msgid "password must contain at least %d characters" -msgstr "Le mot de passe doit contenir au moins %d caractères." - -#: authentic2/validators.py:104 -#, python-format -msgid "" -"password must contain characters from at least %d classes among: lowercase " -"letters, uppercase letters, digits, and punctuations" -msgstr "" -"Le mot de passe doit contenir des caractères d'au moins %d types parmi: " -"minuscules, majuscules, chiffres et ponctuations." - -#: authentic2/validators.py:110 -#, python-format -msgid "your password dit not match the regular expession %s" -msgstr "Votre mot de passe ne valide pas l'expression régulière %s." - -#: authentic2/validators.py:125 -#, python-format -msgid "" -"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." -msgstr "" -"Le mot de passe doit contenir au moins %(min_length)d caractères d'au moins " -"%(min_classes)d types parmi : minuscules, majuscules, chiffres et " -"ponctuation." - -#: authentic2/validators.py:132 -#, python-format -msgid "Your password must contain at least %(min_length)d characters." -msgstr "Le mot de passe doit contenir au moins %(min_length)d caractères." - -#: authentic2/validators.py:134 -#, python-format -msgid "" -"Your password must contain characters from at least %(min_classes)d classes " -"among: lowercase letters, uppercase letters, digits and punctuations." +msgid "Password must obey the rule: %s" msgstr "" -"Le mot de passe doit contenir des caractères d'au moins %(min_classes)d " -"types parmi: minuscules, majuscules, chiffres et ponctuations." -#: authentic2/validators.py:139 -#, python-format -msgid "" -"Your password must match the regular expression: %(regexp)s, please change " -"this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting." +#: authentic2/validators.py:102 +msgid "Password must obey the rules: " msgstr "" -"Votre mot de passe ne valide pas l'expression régulière %(regexp)s. Veuillez " -"changer ce message en modifiant le paramètre " -"A2_PASSWORD_POLICY_REGEX_ERROR_MSG." #: authentic2/views.py:175 msgid "Email Change" @@ -1604,6 +1617,39 @@ msgstr "" msgid "Format:" msgstr "Format :" +#~ msgid "password must contain at least %d characters" +#~ msgstr "Le mot de passe doit contenir au moins %d caractères." + +#~ msgid "" +#~ "password must contain characters from at least %d classes among: " +#~ "lowercase letters, uppercase letters, digits, and punctuations" +#~ msgstr "" +#~ "Le mot de passe doit contenir des caractères d'au moins %d types parmi: " +#~ "minuscules, majuscules, chiffres et ponctuations." + +#~ msgid "your password dit not match the regular expression %s" +#~ msgstr "Votre mot de passe ne valide pas l'expression régulière %s." + +#~ msgid "" +#~ "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." +#~ msgstr "" +#~ "Le mot de passe doit contenir au moins %(min_length)d caractères d'au " +#~ "moins %(min_classes)d types parmi : minuscules, majuscules, chiffres et " +#~ "ponctuation." + +#~ msgid "Your password must contain at least %(min_length)d characters." +#~ msgstr "Le mot de passe doit contenir au moins %(min_length)d caractères." + +#~ msgid "" +#~ "Your password must contain characters from at least %(min_classes)d " +#~ "classes among: lowercase letters, uppercase letters, digits and " +#~ "punctuations." +#~ msgstr "" +#~ "Le mot de passe doit contenir des caractères d'au moins %(min_classes)d " +#~ "types parmi: minuscules, majuscules, chiffres et ponctuations." + #~ msgid "Modify" #~ msgstr "Modifier" diff --git a/src/authentic2/passwords.py b/src/authentic2/passwords.py index 7adea25b..01b0f449 100644 --- a/src/authentic2/passwords.py +++ b/src/authentic2/passwords.py @@ -76,22 +76,22 @@ class DefaultPasswordChecker(PasswordChecker): if self.min_length: yield self.Check( result=len(password) >= self.min_length, - label=_('at least %s characters') % self.min_length) + label=_('%s characters') % self.min_length) if self.at_least_one_lowercase: yield self.Check( result=any(c.islower() for c in password), - label=_('at least 1 lowercase letter')) + label=_('1 lowercase letter')) if self.at_least_one_digit: yield self.Check( result=any(c.isdigit() for c in password), - label=_('at least 1 digit')) + label=_('1 digit')) if self.at_least_one_uppercase: yield self.Check( result=any(c.isupper() for c in password), - label=_('at least 1 uppercase letter')) + label=_('1 uppercase letter')) if self.regexp and self.regexp_label: yield self.Check( diff --git a/src/authentic2/registration_backend/forms.py b/src/authentic2/registration_backend/forms.py index 8ca7d9c5..8f5fc0b9 100644 --- a/src/authentic2/registration_backend/forms.py +++ b/src/authentic2/registration_backend/forms.py @@ -1,6 +1,7 @@ import re import copy from collections import OrderedDict +import json from django.conf import settings from django.core.exceptions import ValidationError @@ -15,10 +16,10 @@ from django.contrib.auth import forms as auth_forms, get_user_model, REDIRECT_FI from django.core.mail import send_mail from django.core import signing from django.template import RequestContext -from django.template.loader import render_to_string from django.core.urlresolvers import reverse from django.core.validators import RegexValidator +from .widgets import CheckPasswordInput, NewPasswordInput from .. import app_settings, compat, forms, utils, validators, models, middleware, hooks from authentic2.a2_rbac.models import OrganizationalUnit @@ -115,10 +116,11 @@ class RegistrationCompletionFormNoPassword(forms.BaseUserForm): class RegistrationCompletionForm(RegistrationCompletionFormNoPassword): - password1 = CharField(widget=PasswordInput, label=_("Password"), - validators=[validators.validate_password], - help_text=validators.password_help_text()) - password2 = CharField(widget=PasswordInput, label=_("Password (again)")) + + password1 = CharField(widget=NewPasswordInput(), label=_("Password"), + validators=[validators.validate_password], + help_text=validators.password_help_text()) + password2 = CharField(widget=CheckPasswordInput(), label=_("Password (again)")) def clean(self): """ diff --git a/src/authentic2/registration_backend/widgets.py b/src/authentic2/registration_backend/widgets.py new file mode 100644 index 00000000..ab84c62f --- /dev/null +++ b/src/authentic2/registration_backend/widgets.py @@ -0,0 +1,92 @@ +from django.forms import PasswordInput +from django.template.loader import render_to_string +from django.utils.encoding import force_text +from django.utils.safestring import mark_safe + +from .. import app_settings + + +class BasePasswordInput(PasswordInput): + """ + a password Input with some features to help the user choosing a new password + Inspired by Django >= 1.11 new-style rendering + (cf. https://docs.djangoproject.com/fr/1.11/ref/forms/renderers) + """ + template_name = 'authentic2/widgets/assisted_password.html' + features = {} + + class Media: + js = ('authentic2/js/password.js',) + css = { + 'all': ('authentic2/css/password.css',) + } + + def get_context(self, name, value, attrs): + """ + Base get_context + """ + context = { + 'app_settings': { + '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, + }, + 'features': self.features + } + # attach data-* attributes for password.js to activate events + attrs.update(dict([('data-%s' % feat.replace('_', '-'), is_active) for feat, is_active in self.features.items()])) + + context['widget'] = { + 'name': name, + 'is_hidden': self.is_hidden, + 'required': self.is_required, + 'template_name': self.template_name, + 'attrs': self.build_attrs(extra_attrs=attrs, name=name, type=self.input_type) + } + # Only add the 'value' attribute if a value is non-empty. + if value is None: + value = '' + if value != '': + context['widget']['value'] = force_text(self._format_value(value)) + + return context + + def render(self, name, value, attrs=None, **kwargs): + """ + Override render with a template-based system + Remove this line when dropping Django 1.8, 1.9, 1.10 compatibility + """ + return mark_safe(render_to_string(self.template_name, + self.get_context(name, value, attrs))) + + +class CheckPasswordInput(BasePasswordInput): + """ + Password typing assistance widget (eg. password2) + """ + features = { + 'check_equality': True, + 'show_all': app_settings.A2_PASSWORD_WIDGET_SHOW_ALL_BUTTON, + 'show_last': True, + } + + def get_context(self, name, value, attrs): + context = super(CheckPasswordInput, self).get_context( + name, value, attrs) + return context + + +class NewPasswordInput(CheckPasswordInput): + """ + Password creation assistance widget with policy (eg. password1) + """ + features = { + 'check_equality': False, + 'show_all': app_settings.A2_PASSWORD_WIDGET_SHOW_ALL_BUTTON, + 'show_last': True, + 'check_policy': True, + } + + def get_context(self, name, value, attrs): + context = super(NewPasswordInput, self).get_context(name, value, attrs) + return context diff --git a/src/authentic2/static/authentic2/css/password.css b/src/authentic2/static/authentic2/css/password.css new file mode 100644 index 00000000..3fdd712f --- /dev/null +++ b/src/authentic2/static/authentic2/css/password.css @@ -0,0 +1,140 @@ +/* required in order to position a2-password-show-all and a2-password-show-last */ +input[type=password].a2-password-assisted { + padding-right: 60px; + width: 100%; +} + +.a2-password-icon { + display: inline-block; + width: calc(18em / 14); + text-align: center; + font-style: normal; + padding-right: 1em; +} + +/* default circle icon */ +.a2-password-icon:before { + font-family: FontAwesome; + content: "\f111"; /* right hand icon */ + font-size: 50%; +} + +.a2-password-policy-helper { + display: flex; + height: auto; + flex-direction: row; + flex-wrap: wrap; + position: relative; + padding: 0.5rem 1rem; + width: 90%; +} + +/* we don't want helptext when a2-password-policy-helper is here */ +.a2-password-policy-helper ~ .helptext { + display: none; +} + +.a2-password-policy-rule { + flex: 1 1 50%; + list-style: none; +} + +.password-error { + color: black; +} + +.password-ok { + color: green; +} + +.password-error .a2-password-icon:before { + content: "\f00d"; /* cross icon */ + color: red; +} + +.password-ok .a2-password-icon::before { + content: "\f00c"; /* ok icon */ + color: green; +} + +.a2-password-show-last { + position: relative; + display: inline-block; + float: right; + opacity: 0; + text-align: center; + right: 10px; + top: -4.5ex; + width: 20px; +} + +.a2-password-show-button { + position: relative; + display: inline-block; + float: right; + padding: 0; + right: 10px; + top: -4.4ex; + cursor: pointer; + width: 20px; +} + +.a2-password-show-button:after { + content: "\f06e"; /* eye */ + font-family: FontAwesome; + font-size: 125%; +} + +.hide-password-button:after { + content: "\f070"; /* crossed eye */ + font-family: FontAwesome; + font-size: 125%; +} + +.a2-passwords-messages { + display: block; + padding: 0.5rem 1rem; +} + +.a2-passwords-default { + list-style: none; + opacity: 0; +} + +.password-error .a2-passwords-default, +.password-ok .a2-passwords-default { + display: none; +} + +.a2-passwords-matched, +.a2-passwords-unmatched { + display: none; + list-style: none; + opacity: 0; + transition: all 0.3s ease; +} + +.password-error.a2-passwords-messages:before, +.password-ok.a2-passwords-messages:before { + display: none; +} + +.password-error .a2-passwords-unmatched, +.password-ok .a2-passwords-matched { + display: block; + opacity: 1; +} + +.password-error .a2-passwords-unmatched .a2-password-icon:before { + content: "\f00d"; /* cross icon */ + color: red; +} + +.password-ok .a2-passwords-matched .a2-password-icon:before { + content: "\f00c"; /* ok icon */ + color: green; +} + +.a2-password-policy-intro { + margin: 0; +} \ No newline at end of file diff --git a/src/authentic2/static/authentic2/css/style.css b/src/authentic2/static/authentic2/css/style.css index c7545cae..c729213c 100644 --- a/src/authentic2/static/authentic2/css/style.css +++ b/src/authentic2/static/authentic2/css/style.css @@ -76,3 +76,22 @@ .a2-log-message { white-space: pre-wrap; } + +.a2-registration-completion { + padding: 1rem; + min-width: 320px; + width: 50%; +} + +@media screen and (max-width: 800px) { + .a2-registration-completion { + width: 100%; + } +} + +.a2-registration-completion input, +.a2-registration-completion select, +.a2-registration-completion textarea +{ + width: 100%; +} \ 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..a29ab593 --- /dev/null +++ b/src/authentic2/static/authentic2/js/password.js @@ -0,0 +1,181 @@ +"use strict"; +/* globals $, window, console */ + +$(function () { + var debounce = function (func, milliseconds) { + var timer; + return function() { + window.clearTimeout(timer); + timer = window.setTimeout(function() { + func(); + }, milliseconds); + }; + } + var toggleError = function($elt) { + $elt.removeClass('password-ok'); + $elt.addClass('password-error'); + } + var toggleOk = function($elt) { + $elt.removeClass('password-error'); + $elt.addClass('password-ok'); + } + /* + * toggle error/ok on element with class names same as the validation code names + * (cf. error_codes in authentic2.validators.validate_password) + */ + var validatePassword = function(event) { + var $this = $(event.target); + if (!$this.val()) return; + var password = $this.val(); + var inputName = $this.attr('name'); + getValidation(password, inputName); + } + var getValidation = function(password, inputName) { + var policyContainer = $('#a2-password-policy-helper-' + inputName); + $.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) { + policyContainer + .empty() + .removeClass('password-error password-ok'); + data.checks.forEach(function (error) { + var $li = $('
  • ') + .html('' + error.label) + .appendTo(policyContainer); + if (!error.result) { + toggleError($li); + } else { + toggleOk($li); + } + }); + } + } + }); + } + /* + * Check password equality + */ + var displayPasswordEquality = function($input, $inputTarget) { + var messages = $('#a2-password-equality-helper-' + $input.attr('name')); + var form = $input.parents('form'); + if ($inputTarget === undefined) { + $inputTarget = form.find('input[type=password]:not(input[name='+$input.attr('name')+'])'); + } + if (!$input.val() || !$inputTarget.val()) return; + if ($inputTarget.val() !== $input.val()) { + toggleError(messages); + } else { + toggleOk(messages); + } + } + var passwordEquality = function () { + var $this = $(this); + displayPasswordEquality($this); + } + /* + * Hide and show password handlers + */ + var showPassword = function (event) { + var $this = $(event.target); + $this.addClass('hide-password-button'); + var name = $this.attr('id').split('a2-password-show-button-')[1]; + $('[name='+name+']').attr('type', 'text'); + event.preventDefault(); + } + var hidePassword = function (event) { + var $this = $(event.target); + window.setTimeout(function () { + $this.removeClass('hide-password-button'); + var name = $this.attr('id').split('a2-password-show-button-')[1]; + $('[name='+name+']').attr('type', 'password'); + }, 3000); + } + /* + * Show the last character + */ + var showLastChar = function(event) { + if (event.keyCode == 32 || event.key === undefined || event.key == "" + || event.key == "Unidentified" || event.key.length > 1) { + return; + } + var duration = 1000; + $('#a2-password-show-last-'+$(event.target).attr('name')) + .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); + } + }); + } + /* + * Init events + */ + /* add password validation and equality check event handlers */ + $('form input[type=password]:not(input[data-check-policy])').each(function () { + $('#a2-password-policy-helper-' + $(this).attr('name')).hide(); + }); + $('body').on('keyup', 'form input[data-check-policy]', validatePassword); + $('body').on('keyup', 'form input[data-check-equality]', passwordEquality); + /* + * Add event to handle displaying error/OK + * while editing the first password + * only if the second one is not empty + */ + $('input[data-check-equality]') + .each(function () { + var $input2 = $(this); + $('body') + .on('keyup', 'form input[type=password]:not([name=' + $input2.attr('name') + '])', + function (event) { + var $input1 = $(event.target); + if ($input2.val().length) { + displayPasswordEquality($input2, $input1); + } + }); + }); + /* add the a2-password-show-button after the first input */ + $('input[data-show-all]') + .each(function () { + var $this = $(this); + if (!$('#a2-password-show-button-' + $this.attr('name')).length) { + $(this).after($('') + .on('mousedown', showPassword) + .on('mouseup mouseleave', hidePassword) + ); + } + }); + /* show the last character on keypress */ + $('input[data-show-last]') + .each(function () { + var $this = $(this); + if (!$('#a2-password-show-last-' + $this.attr('name')).length) { + // on crée un div placé dans le padding-right de l'input + var $span = $(')') + $span.css({ + 'font-size': $this.css('font-size'), + 'font-family': $this.css('font-family'), + 'line-height': parseInt($this.css('line-height').replace('px', '')) - parseInt($this.css('padding-bottom').replace('px', '')) + 'px', + 'vertical-align': $this.css('vertical-align'), + 'padding-top': $this.css('padding-top'), + 'padding-bottom': $this.css('padding-bottom') + }); + $this.after($span); + } + }); + $('body').on('keyup', 'form input[data-show-last]', showLastChar); +}); diff --git a/src/authentic2/templates/authentic2/widgets/assisted_password.html b/src/authentic2/templates/authentic2/widgets/assisted_password.html new file mode 100644 index 00000000..97fa29aa --- /dev/null +++ b/src/authentic2/templates/authentic2/widgets/assisted_password.html @@ -0,0 +1,34 @@ +{% load i18n %} + +{% if features.check_policy %} +

    {% blocktrans %}In order to create a secure password, please use at least : {% endblocktrans %}

    + +{% endif %} +{% if features.check_equality %} + +{% endif %} diff --git a/src/authentic2/templates/authentic2/widgets/attrs.html b/src/authentic2/templates/authentic2/widgets/attrs.html new file mode 100644 index 00000000..6fba365e --- /dev/null +++ b/src/authentic2/templates/authentic2/widgets/attrs.html @@ -0,0 +1,2 @@ +{% comment %}Will be deprecated in Django 1.11 : replace with django/forms/widgets/attrs.html{% endcomment %} +{% for name, value in widget.attrs.items %}{% if value != False %} {{ name }}{% if value != True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %} \ No newline at end of file diff --git a/src/authentic2/templates/registration/registration_completion_form.html b/src/authentic2/templates/registration/registration_completion_form.html index cd179aaa..1ab0d5ac 100644 --- a/src/authentic2/templates/registration/registration_completion_form.html +++ b/src/authentic2/templates/registration/registration_completion_form.html @@ -25,7 +25,7 @@ {% block content %}

    {% trans "Registration" %}

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

    -
    + {% csrf_token %} {{ form.as_p }} diff --git a/tests/test_api.py b/tests/test_api.py index 0065670f..acb406c3 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -834,7 +834,7 @@ def test_validate_password_default(app): ('x' * 8, False, True, True, False, False), ('x' * 8 + '1', False, True, True, True, False), ('x' * 8 + '1X', True, True, True, True, True)): - response = app.post_json('/api/validate-password/', params={'password': password}) + response = app.post_json('/api/validate-password/', params={'password': password}) assert response.json['result'] == 1 assert response.json['ok'] is ok assert len(response.json['checks']) == 4 diff --git a/tests/test_registration.py b/tests/test_registration.py index be915ecc..61587993 100644 --- a/tests/test_registration.py +++ b/tests/test_registration.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import re from urlparse import urlparse from django.core.urlresolvers import reverse @@ -585,3 +586,49 @@ def test_registration_redirect_tuple(app, db, settings, mailoutbox, external_red response = response.form.submit() assert new_next_url in response.content + +def test_registration_activate_passwords_not_equal(app, db, settings, mailoutbox): + settings.LANGUAGE_CODE = 'en-us' + settings.A2_VALIDATE_EMAIL_DOMAIN = can_resolve_dns() + settings.A2_EMAIL_IS_UNIQUE = True + + response = app.get(reverse('registration_register')) + response.form.set('email', 'testbot@entrouvert.com') + response = response.form.submit() + response = response.follow() + link = get_link_from_mail(mailoutbox[0]) + response = app.get(link) + response.form.set('password1', 'azerty12AZ') + response.form.set('password2', 'AAAazerty12AZ') + response = response.form.submit() + assert "The two password fields didn't match." in response.content + + +def test_registration_activate_assisted_password(app, db, settings, mailoutbox): + response = app.get(reverse('registration_register')) + response.form.set('email', 'testbot@entrouvert.com') + response = response.form.submit() + response = response.follow() + link = get_link_from_mail(mailoutbox[0]) + response = app.get(link) + # check presence of the script and css for RegistrationCompletionForm to work + assert "password.js" in response.content + assert "password.css" in response.content + # check default attributes for password.js and css to work + assert re.search('', response.content, re.I | re.M | re.S) + assert re.search('', response.content, re.I | re.M | re.S) + assert re.search('', response.content, re.I | re.M | re.S) + # check template containers for password.js to display its results + assert re.search('class="a2-passwords-messages" id="a2-password-equality-helper-', response.content, re.I | re.M | re.S) + assert re.search('class="a2-password-policy-helper" id="a2-password-policy-helper-', response.content, re.I | re.M | re.S) + assert re.search('class="a2-password-policy-rule"', response.content, re.I | re.M | re.S) + + +def test_registration_activate_password_no_show_all_button(app, db, settings, mailoutbox): + response = app.get(reverse('registration_register')) + response.form.set('email', 'testbot@entrouvert.com') + response = response.form.submit() + response = response.follow() + link = get_link_from_mail(mailoutbox[0]) + response = app.get(link) + assert not re.search('', response.content, re.I | re.M | re.S) \ No newline at end of file -- 2.18.0