Projet

Général

Profil

0001-create-assisted-password-input-widgets-24438.patch

basé sur les patchs de #24833 - Anonyme, 06 juillet 2018 17:51

Télécharger (38,3 ko)

Voir les différences:

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
src/authentic2/app_settings.py
143 143
    A2_PASSWORD_POLICY_MIN_LENGTH=Setting(default=8, definition='Minimum number of characters in a password'),
144 144
    A2_PASSWORD_POLICY_REGEX=Setting(default=None, definition='Regular expression for validating passwords'),
145 145
    A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting(default=None, definition='Error message to show when the password do not validate the regular expression'),
146
    A2_PASSWORD_WIDGET_SHOW_ALL_BUTTON=Setting(default=False, definition='Show a button on BasePasswordInput for the user to see password input text'),
146 147
    A2_PASSWORD_POLICY_CLASS=Setting(
147 148
        default='authentic2.passwords.DefaultPasswordChecker',
148 149
        definition='path of a class to validate passwords'),
src/authentic2/locale/fr/LC_MESSAGES/django.po
7 7
msgstr ""
8 8
"Project-Id-Version: Authentic\n"
9 9
"Report-Msgid-Bugs-To: \n"
10
"POT-Creation-Date: 2018-07-05 16:39+0200\n"
10
"POT-Creation-Date: 2018-07-06 16:53+0200\n"
11 11
"PO-Revision-Date: 2018-07-05 14:08+0200\n"
12 12
"Last-Translator: Mikaël Ates <mates@entrouvert.com>\n"
13 13
"Language-Team: None\n"
......
17 17
"Content-Transfer-Encoding: 8bit\n"
18 18
"Plural-Forms: nplurals=2; plural=n>1;\n"
19 19

  
20
#: authentic2/admin.py:26
20
#: debian-jessie/multitenant/debian_config.py:37
21
#: debian-wheezy/multitenant/debian_config.py:38
22
#: authentic2/profile_forms.py:19
23
#: authentic2/registration_backend/forms.py:33
24
#: authentic2/templates/authentic2/api_user_create_registration_email_body.txt:4
25
msgid "Email"
26
msgstr "Courriel"
21 27
msgid "Cleanup expired objects"
22 28
msgstr "Nettoyer les objets qui ont expiré"
23 29

  
......
63 69
msgid "You must at least give a username or an email to your user"
64 70
msgstr "Un utilisateur doit au minimum posséder un courriel ou un identifiant."
65 71

  
66
#: authentic2/admin.py:167 authentic2/admin.py:204
67
#: authentic2/auth_frontends.py:14 authentic2/forms.py:24
68
#: authentic2/registration_backend/forms.py:118
69
#: authentic2/registration_backend/forms.py:204
70
#: authentic2/templates/authentic2/login_password_profile.html:4
72
#: admin.py:167 admin.py:204
73
#: auth_frontends.py:14 forms.py:24
74
#: registration_backend/forms.py:120
75
#: registration_backend/forms.py:206
76
#: templates/authentic2/login_password_profile.html:4
71 77
msgid "Password"
72 78
msgstr "Mot de passe"
73 79

  
......
82 88
"modifier le mot de passe de l'usager en utilisant <a href=\"password/\">ce "
83 89
"formulaire</a>."
84 90

  
85
#: authentic2/admin.py:201 authentic2/registration_backend/forms.py:132
91
#: admin.py:201 registration_backend/forms.py:134
86 92
msgid "The two password fields didn't match."
87 93
msgstr "Les deux champs mot de passe ne correspondent pas."
88 94

  
......
114 120
msgid "you are not authorized to create users in this ou"
115 121
msgstr "Vous n'êtes pas autorisé à vous inscrire dans cette collectivité."
116 122

  
117
#: authentic2/api_views.py:119 authentic2/api_views.py:126
123
#: authentic2/api_views.py:120 authentic2/api_views.py:127
118 124
msgid "You already have an account"
119 125
msgstr "Vous avez déjà un compte."
120 126

  
121
#: authentic2/api_views.py:123
127
#: authentic2/api_views.py:124 tests/test_all.py:530 tests/test_all.py:675
122 128
msgid "Username is required in this ou"
123 129
msgstr "L'identifiant est requis dans cette collectivité."
124 130

  
125
#: authentic2/api_views.py:654
131
#: authentic2/api_views.py:655
126 132
msgid "User successfully added to role"
127 133
msgstr "Utilisateur ajouté au rôle"
128 134

  
129
#: authentic2/api_views.py:659
135
#: authentic2/api_views.py:660
130 136
msgid "User successfully removed from role"
131 137
msgstr "Utilisateur retiré du rôle"
132 138

  
......
596 602
msgid "base service models"
597 603
msgstr "applications"
598 604

  
599
#: authentic2/profile_forms.py:19 authentic2/registration_backend/forms.py:32
600
#: authentic2/templates/authentic2/api_user_create_registration_email_body.txt:4
601
msgid "Email"
602
msgstr "Courriel"
605
#: authentic2/passwords.py:80
606
#, python-format
607
msgid "%s characters"
608
msgstr "%s caractères"
609

  
610
#: authentic2/passwords.py:85
611
#: authentic2/templates/authentic2/widgets/assisted_password.html:12
612
msgid "1 lowercase letter"
613
msgstr "1 minuscule"
614

  
615
#: authentic2/passwords.py:90
616
#: authentic2/templates/authentic2/widgets/assisted_password.html:15
617
msgid "1 digit"
618
msgstr "1 chiffre"
619

  
620
#: authentic2/passwords.py:95
621
#: authentic2/templates/authentic2/widgets/assisted_password.html:18
622
msgid "1 uppercase letter"
623
msgstr "1 majuscule"
603 624

  
604 625
#: authentic2/profile_urls.py:41
605 626
#: authentic2/templates/registration/password_change_done.html:9
......
646 667
msgid "Enter new password"
647 668
msgstr "Entrez un nouveau mot de passe"
648 669

  
649
#: authentic2/registration_backend/forms.py:52
670
#: authentic2/registration_backend/forms.py:53
650 671
msgid "You cannot register with this email."
651 672
msgstr "Vous ne pouvez pas vous inscrire avec cette adresse de courriel."
652 673

  
653
#: authentic2/registration_backend/forms.py:81
674
#: authentic2/registration_backend/forms.py:82 tests/test_all.py:763
654 675
msgid "This username is already in use. Please supply a different username."
655 676
msgstr ""
656 677
"Cet identifiant est déjà utilisé. Utilisez s'il vous plait un autre "
657 678
"identifiant."
658 679

  
659
#: authentic2/registration_backend/forms.py:100
680
#: authentic2/registration_backend/forms.py:101
660 681
msgid ""
661 682
"This email address is already in use. Please supply a different email "
662 683
"address."
......
664 685
"Cette adresse de courriel est déjà utilisée. Utilisez s'il vous plait une "
665 686
"autre adresse de courriel."
666 687

  
667
#: authentic2/registration_backend/forms.py:121
688
#: authentic2/registration_backend/forms.py:123
668 689
msgid "Password (again)"
669 690
msgstr "Confirmation du mot de passe"
670 691

  
671
#: authentic2/registration_backend/forms.py:168
672
#: authentic2/registration_backend/forms.py:182
692
#: authentic2/registration_backend/forms.py:170
693
#: authentic2/registration_backend/forms.py:184
673 694
msgid "New password"
674 695
msgstr "Nouveau mot de passe"
675 696

  
676
#: authentic2/registration_backend/forms.py:176
677
#: authentic2/registration_backend/forms.py:191
697
#: authentic2/registration_backend/forms.py:178
698
#: authentic2/registration_backend/forms.py:193
678 699
msgid "New password must differ from old password"
679 700
msgstr "Le nouveau mot de passe doit être différent de l'ancien."
680 701

  
681
#: authentic2/registration_backend/forms.py:213
702
#: authentic2/registration_backend/forms.py:215
682 703
msgid "Password is invalid"
683 704
msgstr "Le mot de passe est invalide"
684 705

  
685
#: authentic2/registration_backend/views.py:44
706
#: authentic2/registration_backend/views.py:45
686 707
msgid "Your activation key is expired"
687 708
msgstr "Votre clé d'activation a expiré"
688 709

  
689
#: authentic2/registration_backend/views.py:47
710
#: authentic2/registration_backend/views.py:48
690 711
msgid "Activation failed"
691 712
msgstr "Échec à l'activation du compte"
692 713

  
693
#: authentic2/registration_backend/views.py:56
714
#: authentic2/registration_backend/views.py:57
694 715
#: authentic2/templates/registration/registration_completion_choose.html:6
695 716
#: authentic2/templates/registration/registration_completion_form.html:17
696 717
#: authentic2/templates/registration/registration_completion_form.html:26
697 718
msgid "Registration"
698 719
msgstr "Création d'un compte"
699 720

  
700
#: authentic2/registration_backend/views.py:344
721
#: authentic2/registration_backend/views.py:345
701 722
msgid "You have just created an account."
702 723
msgstr "Vous venez de créer un compte."
703 724

  
704
#: authentic2/registration_backend/views.py:369
725
#: authentic2/registration_backend/views.py:370
705 726
#: authentic2/templates/authentic2/accounts.html:41
706 727
msgid "Delete account"
707 728
msgstr "Supprimer votre compte"
708 729

  
709
#: authentic2/registration_backend/views.py:400
730
#: authentic2/registration_backend/views.py:401
710 731
msgid ""
711 732
"Your account has been scheduled for deletion. You cannot use it anymore."
712 733
msgstr ""
......
1133 1154
msgid "Notification: %(user)s, your account has been deleted"
1134 1155
msgstr "Notification: %(user)s, votre compte a été supprimé"
1135 1156

  
1136
#: authentic2/templates/error_ssl.html:4
1157
#: templates/authentic2/widgets/assisted_password.html:4
1158
msgid "In order to create a secure password, please use <i>at least</i> : "
1159
msgstr ""
1160
"Pour avoir un mot de passe sécurisé, veuillez utiliser <i>à minima</i> :"
1161

  
1162
#: authentic2/templates/authentic2/widgets/assisted_password.html:9
1163
#, python-format
1164
msgid "%(A2_PASSWORD_POLICY_MIN_LENGTH)s characters"
1165
msgstr "%(A2_PASSWORD_POLICY_MIN_LENGTH)s caratères"
1166

  
1167
#: templates/authentic2/widgets/assisted_password.html:16
1168
#, python-format
1169
msgid "%(A2_PASSWORD_POLICY_REGEX_ERROR_MSG)s"
1170
msgstr "%(A2_PASSWORD_POLICY_REGEX_ERROR_MSG)s"
1171

  
1172
#: authentic2/templates/authentic2/widgets/assisted_password.html:24
1173
#, python-format
1174
msgid ""
1175
"Match the regular expression: %(A2_PASSWORD_POLICY_REGEX)s, please change "
1176
"this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.'"
1177
msgstr ""
1178
"Valider l'expression régulière %(A2_PASSWORD_POLICY_REGEX)s. Veuillez "
1179
"changer ce message en modifiant le paramètre "
1180
"A2_PASSWORD_POLICY_REGEX_ERROR_MSG."
1181

  
1182
#: authentic2/templates/authentic2/widgets/assisted_password.html:31
1183
msgid "Both passwords must match."
1184
msgstr "Les deux mots de passe doivent être identiques."
1185

  
1186
#: authentic2/templates/authentic2/widgets/assisted_password.html:32
1187
msgid "Passwords match."
1188
msgstr "Les mots de passe sont identiques."
1189

  
1190
#: authentic2/templates/authentic2/widgets/assisted_password.html:33
1191
msgid "Passwords do not match."
1192
msgstr "Les deux mots de passe ne sont pas identiques."
1193

  
1194
#: templates/error_ssl.html:4
1137 1195
msgid "Error: authentication failure"
1138 1196
msgstr "Erreur: échec de l'authentification"
1139 1197

  
......
1488 1546

  
1489 1547
#: authentic2/validators.py:95
1490 1548
#, python-format
1491
msgid "password must contain at least %d characters"
1492
msgstr "Le mot de passe doit contenir au moins %d caractères."
1493

  
1494
#: authentic2/validators.py:104
1495
#, python-format
1496
msgid ""
1497
"password must contain characters from at least %d classes among: lowercase "
1498
"letters, uppercase letters, digits, and punctuations"
1499
msgstr ""
1500
"Le mot de passe doit contenir des caractères d'au moins %d types parmi: "
1501
"minuscules, majuscules, chiffres et ponctuations."
1502

  
1503
#: authentic2/validators.py:110
1504
#, python-format
1505
msgid "your password dit not match the regular expession %s"
1506
msgstr "Votre mot de passe ne valide pas l'expression régulière %s."
1507

  
1508
#: authentic2/validators.py:125
1509
#, python-format
1510
msgid ""
1511
"Your password must contain at least %(min_length)d characters from at least "
1512
"%(min_classes)d classes among: lowercase letters, uppercase letters, digits "
1513
"and punctuations."
1514
msgstr ""
1515
"Le mot de passe doit contenir au moins %(min_length)d caractères d'au moins "
1516
"%(min_classes)d types parmi : minuscules, majuscules, chiffres et "
1517
"ponctuation."
1518

  
1519
#: authentic2/validators.py:132
1520
#, python-format
1521
msgid "Your password must contain at least %(min_length)d characters."
1522
msgstr "Le mot de passe doit contenir au moins %(min_length)d caractères."
1523

  
1524
#: authentic2/validators.py:134
1525
#, python-format
1526
msgid ""
1527
"Your password must contain characters from at least %(min_classes)d classes "
1528
"among: lowercase letters, uppercase letters, digits and punctuations."
1549
msgid "Password must obey the rule: %s"
1529 1550
msgstr ""
1530
"Le mot de passe doit contenir des caractères d'au moins %(min_classes)d "
1531
"types parmi: minuscules, majuscules, chiffres et ponctuations."
1532 1551

  
1533
#: authentic2/validators.py:139
1534
#, python-format
1535
msgid ""
1536
"Your password must match the regular expression: %(regexp)s, please change "
1537
"this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting."
1552
#: authentic2/validators.py:102
1553
msgid "Password must obey the rules: "
1538 1554
msgstr ""
1539
"Votre mot de passe ne valide pas l'expression régulière %(regexp)s. Veuillez "
1540
"changer ce message en modifiant le paramètre "
1541
"A2_PASSWORD_POLICY_REGEX_ERROR_MSG."
1542 1555

  
1543 1556
#: authentic2/views.py:175
1544 1557
msgid "Email Change"
......
1604 1617
msgid "Format:"
1605 1618
msgstr "Format :"
1606 1619

  
1620
#~ msgid "password must contain at least %d characters"
1621
#~ msgstr "Le mot de passe doit contenir au moins %d caractères."
1622

  
1623
#~ msgid ""
1624
#~ "password must contain characters from at least %d classes among: "
1625
#~ "lowercase letters, uppercase letters, digits, and punctuations"
1626
#~ msgstr ""
1627
#~ "Le mot de passe doit contenir des caractères d'au moins %d types parmi: "
1628
#~ "minuscules, majuscules, chiffres et ponctuations."
1629

  
1630
#~ msgid "your password dit not match the regular expression %s"
1631
#~ msgstr "Votre mot de passe ne valide pas l'expression régulière %s."
1632

  
1633
#~ msgid ""
1634
#~ "Your password must contain at least %(min_length)d characters from at "
1635
#~ "least %(min_classes)d classes among: lowercase letters, uppercase "
1636
#~ "letters, digits and punctuations."
1637
#~ msgstr ""
1638
#~ "Le mot de passe doit contenir au moins %(min_length)d caractères d'au "
1639
#~ "moins %(min_classes)d types parmi : minuscules, majuscules, chiffres et "
1640
#~ "ponctuation."
1641

  
1642
#~ msgid "Your password must contain at least %(min_length)d characters."
1643
#~ msgstr "Le mot de passe doit contenir au moins %(min_length)d caractères."
1644

  
1645
#~ msgid ""
1646
#~ "Your password must contain characters from at least %(min_classes)d "
1647
#~ "classes among: lowercase letters, uppercase letters, digits and "
1648
#~ "punctuations."
1649
#~ msgstr ""
1650
#~ "Le mot de passe doit contenir des caractères d'au moins %(min_classes)d "
1651
#~ "types parmi: minuscules, majuscules, chiffres et ponctuations."
1652

  
1607 1653
#~ msgid "Modify"
1608 1654
#~ msgstr "Modifier"
1609 1655

  
src/authentic2/passwords.py
76 76
        if self.min_length:
77 77
            yield self.Check(
78 78
                result=len(password) >= self.min_length,
79
                label=_('at least %s characters') % self.min_length)
79
                label=_('%s characters') % self.min_length)
80 80

  
81 81
        if self.at_least_one_lowercase:
82 82
            yield self.Check(
83 83
                result=any(c.islower() for c in password),
84
                label=_('at least 1 lowercase letter'))
84
                label=_('1 lowercase letter'))
85 85

  
86 86
        if self.at_least_one_digit:
87 87
            yield self.Check(
88 88
                result=any(c.isdigit() for c in password),
89
                label=_('at least 1 digit'))
89
                label=_('1 digit'))
90 90

  
91 91
        if self.at_least_one_uppercase:
92 92
            yield self.Check(
93 93
                result=any(c.isupper() for c in password),
94
                label=_('at least 1 uppercase letter'))
94
                label=_('1 uppercase letter'))
95 95

  
96 96
        if self.regexp and self.regexp_label:
97 97
            yield self.Check(
src/authentic2/registration_backend/forms.py
1 1
import re
2 2
import copy
3 3
from collections import OrderedDict
4
import json
4 5

  
5 6
from django.conf import settings
6 7
from django.core.exceptions import ValidationError
......
15 16
from django.core.mail import send_mail
16 17
from django.core import signing
17 18
from django.template import RequestContext
18
from django.template.loader import render_to_string
19 19
from django.core.urlresolvers import reverse
20 20
from django.core.validators import RegexValidator
21 21

  
22
from .widgets import CheckPasswordInput, NewPasswordInput
22 23
from .. import app_settings, compat, forms, utils, validators, models, middleware, hooks
23 24
from authentic2.a2_rbac.models import OrganizationalUnit
24 25

  
......
115 116

  
116 117

  
117 118
class RegistrationCompletionForm(RegistrationCompletionFormNoPassword):
118
    password1 = CharField(widget=PasswordInput, label=_("Password"),
119
            validators=[validators.validate_password],
120
            help_text=validators.password_help_text())
121
    password2 = CharField(widget=PasswordInput, label=_("Password (again)"))
119

  
120
    password1 = CharField(widget=NewPasswordInput(), label=_("Password"),
121
        validators=[validators.validate_password],
122
        help_text=validators.password_help_text())
123
    password2 = CharField(widget=CheckPasswordInput(), label=_("Password (again)"))
122 124

  
123 125
    def clean(self):
124 126
        """
src/authentic2/registration_backend/widgets.py
1
from django.forms import PasswordInput
2
from django.template.loader import render_to_string
3
from django.utils.encoding import force_text
4
from django.utils.safestring import mark_safe
5

  
6
from .. import app_settings
7

  
8

  
9
class BasePasswordInput(PasswordInput):
10
    """
11
    a password Input with some features to help the user choosing a new password
12
    Inspired by Django >= 1.11 new-style rendering
13
    (cf. https://docs.djangoproject.com/fr/1.11/ref/forms/renderers)
14
    """
15
    template_name = 'authentic2/widgets/assisted_password.html'
16
    features = {}
17

  
18
    class Media:
19
        js = ('authentic2/js/password.js',)
20
        css = {
21
            'all': ('authentic2/css/password.css',)
22
        }
23

  
24
    def get_context(self, name, value, attrs):
25
        """
26
        Base get_context
27
        """
28
        context = {
29
            'app_settings': {
30
                'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH,
31
                'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES,
32
                'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX,
33
            },
34
            'features': self.features
35
        }
36
        # attach data-* attributes for password.js to activate events
37
        attrs.update(dict([('data-%s' % feat.replace('_', '-'), is_active) for feat, is_active in self.features.items()]))
38

  
39
        context['widget'] = {
40
            'name': name,
41
            'is_hidden': self.is_hidden,
42
            'required': self.is_required,
43
            'template_name': self.template_name,
44
            'attrs': self.build_attrs(extra_attrs=attrs, name=name, type=self.input_type)
45
        }
46
        # Only add the 'value' attribute if a value is non-empty.
47
        if value is None:
48
            value = ''
49
        if value != '':
50
            context['widget']['value'] = force_text(self._format_value(value))
51

  
52
        return context
53

  
54
    def render(self, name, value, attrs=None, **kwargs):
55
        """
56
        Override render with a template-based system
57
        Remove this line when dropping Django 1.8, 1.9, 1.10 compatibility
58
        """
59
        return mark_safe(render_to_string(self.template_name,
60
            self.get_context(name, value, attrs)))
61

  
62

  
63
class CheckPasswordInput(BasePasswordInput):
64
    """
65
    Password typing assistance widget (eg. password2)
66
    """
67
    features = {
68
        'check_equality': True,
69
        'show_all': app_settings.A2_PASSWORD_WIDGET_SHOW_ALL_BUTTON,
70
        'show_last': True,
71
    }
72

  
73
    def get_context(self, name, value, attrs):
74
        context = super(CheckPasswordInput, self).get_context(
75
            name, value, attrs)
76
        return context
77

  
78

  
79
class NewPasswordInput(CheckPasswordInput):
80
    """
81
    Password creation assistance widget with policy (eg. password1)
82
    """
83
    features = {
84
        'check_equality': False,
85
        'show_all': app_settings.A2_PASSWORD_WIDGET_SHOW_ALL_BUTTON,
86
        'show_last': True,
87
        'check_policy': True,
88
    }
89

  
90
    def get_context(self, name, value, attrs):
91
        context = super(NewPasswordInput, self).get_context(name, value, attrs)
92
        return context
src/authentic2/static/authentic2/css/password.css
1
/* required in order to position a2-password-show-all and a2-password-show-last */
2
input[type=password].a2-password-assisted {
3
  padding-right: 60px;
4
  width: 100%;
5
}
6

  
7
.a2-password-icon {
8
	display: inline-block;
9
	width: calc(18em / 14);
10
	text-align: center;
11
	font-style: normal;
12
	padding-right: 1em;
13
}
14

  
15
/* default circle icon */
16
.a2-password-icon:before {
17
	font-family: FontAwesome;
18
  content: "\f111"; /* right hand icon */
19
  font-size: 50%;
20
}
21

  
22
.a2-password-policy-helper {
23
  display: flex;
24
  height: auto;
25
  flex-direction: row;
26
  flex-wrap: wrap;
27
  position: relative;
28
  padding: 0.5rem 1rem;
29
  width: 90%;
30
}
31

  
32
/* we don't want helptext when a2-password-policy-helper is here */
33
.a2-password-policy-helper ~ .helptext {
34
  display: none;
35
}
36

  
37
.a2-password-policy-rule {
38
  flex: 1 1 50%;
39
  list-style: none;
40
}
41

  
42
.password-error {
43
  color: black;
44
}
45

  
46
.password-ok {
47
  color: green;
48
}
49

  
50
.password-error .a2-password-icon:before {
51
  content: "\f00d"; /* cross icon */
52
  color: red;
53
}
54

  
55
.password-ok .a2-password-icon::before {
56
  content: "\f00c"; /* ok icon */
57
  color: green;
58
}
59

  
60
.a2-password-show-last {
61
  position: relative;
62
  display: inline-block;
63
  float: right;
64
  opacity: 0;
65
  text-align: center;
66
  right: 10px;
67
  top: -4.5ex;
68
  width: 20px;
69
}
70

  
71
.a2-password-show-button {
72
  position: relative;
73
  display: inline-block;
74
  float: right;
75
  padding: 0;
76
  right: 10px;
77
  top: -4.4ex;
78
  cursor: pointer;
79
  width: 20px;
80
}
81

  
82
.a2-password-show-button:after {
83
  content: "\f06e"; /* eye */
84
  font-family: FontAwesome;
85
  font-size: 125%;
86
}
87

  
88
.hide-password-button:after {
89
  content: "\f070"; /* crossed eye */
90
  font-family: FontAwesome;
91
  font-size: 125%;
92
}
93

  
94
.a2-passwords-messages {
95
  display: block;
96
  padding: 0.5rem 1rem;
97
}
98

  
99
.a2-passwords-default {
100
	list-style: none;
101
  opacity: 0;
102
}
103

  
104
.password-error .a2-passwords-default,
105
.password-ok .a2-passwords-default {
106
	display: none;
107
}
108

  
109
.a2-passwords-matched,
110
.a2-passwords-unmatched {
111
	display: none;
112
	list-style: none;
113
	opacity: 0;
114
	transition: all 0.3s ease;
115
}
116

  
117
.password-error.a2-passwords-messages:before,
118
.password-ok.a2-passwords-messages:before {
119
  display: none;
120
}
121

  
122
.password-error .a2-passwords-unmatched,
123
.password-ok .a2-passwords-matched {
124
	display: block;
125
	opacity: 1;
126
}
127

  
128
.password-error .a2-passwords-unmatched .a2-password-icon:before {
129
  content: "\f00d"; /* cross icon */
130
  color: red;
131
}
132

  
133
.password-ok .a2-passwords-matched .a2-password-icon:before {
134
  content: "\f00c"; /* ok icon */
135
  color: green;
136
}
137

  
138
.a2-password-policy-intro {
139
  margin: 0;
140
}
src/authentic2/static/authentic2/css/style.css
76 76
.a2-log-message {
77 77
  white-space: pre-wrap;
78 78
}
79

  
80
.a2-registration-completion {
81
  padding: 1rem;
82
  min-width: 320px;
83
  width: 50%;
84
}
85

  
86
@media screen and (max-width: 800px) {
87
  .a2-registration-completion {
88
    width: 100%;
89
  }
90
}
91

  
92
.a2-registration-completion input,
93
.a2-registration-completion select,
94
.a2-registration-completion textarea
95
{
96
  width: 100%;
97
}
src/authentic2/static/authentic2/js/password.js
1
"use strict";
2
/* globals $, window, console */
3

  
4
$(function () {
5
	var debounce = function (func, milliseconds) {
6
		var timer;
7
		return function() {
8
			window.clearTimeout(timer);
9
			timer = window.setTimeout(function() {
10
				func();
11
			}, milliseconds);
12
		};
13
	}
14
	var toggleError = function($elt) {
15
		$elt.removeClass('password-ok');
16
		$elt.addClass('password-error');
17
	}
18
	var toggleOk = function($elt) {
19
		$elt.removeClass('password-error');
20
		$elt.addClass('password-ok');
21
	}
22
	/*
23
	* toggle error/ok on element with class names same as the validation code names
24
	* (cf. error_codes in authentic2.validators.validate_password)
25
	*/
26
	var validatePassword = function(event) {
27
		var $this = $(event.target);
28
		if (!$this.val()) return;
29
		var password = $this.val();
30
		var inputName = $this.attr('name');
31
		getValidation(password, inputName);
32
	}
33
	var getValidation = function(password, inputName) {
34
		var policyContainer = $('#a2-password-policy-helper-' + inputName);
35
		$.ajax({
36
			method: 'POST',
37
			url: '/api/validate-password/',
38
			data: JSON.stringify({'password': password}),
39
			dataType: 'json',
40
			contentType: 'application/json; charset=utf-8',
41
			success: function(data) {
42
				if (data.result) {
43
					policyContainer
44
					.empty()
45
					.removeClass('password-error password-ok');
46
					data.checks.forEach(function (error) {
47
						var $li = $('<li class="a2-password-policy-rule"></li>')
48
							.html('<i class="a2-password-icon"></i>' + error.label)
49
							.appendTo(policyContainer);
50
						if (!error.result) {
51
							toggleError($li);
52
						} else {
53
							toggleOk($li);
54
						}
55
					});
56
				}
57
			}
58
		});
59
	}
60
	/*
61
	* Check password equality
62
	*/
63
	var displayPasswordEquality = function($input, $inputTarget) {
64
		var messages = $('#a2-password-equality-helper-' + $input.attr('name'));
65
		var form = $input.parents('form');
66
		if ($inputTarget === undefined) {
67
			$inputTarget = form.find('input[type=password]:not(input[name='+$input.attr('name')+'])');
68
		}
69
		if (!$input.val() || !$inputTarget.val()) return;
70
		if ($inputTarget.val() !== $input.val()) {
71
			toggleError(messages);
72
		} else {
73
			toggleOk(messages);
74
		}
75
	}
76
	var passwordEquality = function () {
77
		var $this = $(this);
78
		displayPasswordEquality($this);
79
	}
80
	/*
81
	* Hide and show password handlers
82
	*/
83
	var showPassword = function (event) {
84
		var $this = $(event.target);
85
		$this.addClass('hide-password-button');
86
		var name = $this.attr('id').split('a2-password-show-button-')[1];
87
		$('[name='+name+']').attr('type', 'text');
88
		event.preventDefault();
89
	}
90
	var hidePassword = function (event) {
91
		var $this = $(event.target);
92
		window.setTimeout(function () {
93
			$this.removeClass('hide-password-button');
94
			var name = $this.attr('id').split('a2-password-show-button-')[1];
95
			$('[name='+name+']').attr('type', 'password');
96
		}, 3000);
97
	}
98
	/*
99
	* Show the last character
100
	*/
101
	var showLastChar = function(event) {
102
		if (event.keyCode == 32 || event.key === undefined || event.key == ""
103
			|| event.key == "Unidentified" || event.key.length > 1) {
104
			return;
105
		}
106
		var duration = 1000;
107
		$('#a2-password-show-last-'+$(event.target).attr('name'))
108
			.text(event.key)
109
			.animate({'opacity': 1}, {
110
				duration: 50,
111
				queue: false,
112
				complete: function () {
113
					var $this = $(this);
114
					window.setTimeout(
115
						debounce(function () {
116
							$this.animate({'opacity': 0}, {
117
								duration: 50
118
							});
119
						}, duration), duration);
120
				}
121
			});
122
	}
123
	/*
124
	* Init events
125
	*/
126
	/* add password validation and equality check event handlers */
127
	$('form input[type=password]:not(input[data-check-policy])').each(function () {
128
		$('#a2-password-policy-helper-' + $(this).attr('name')).hide();
129
	});
130
	$('body').on('keyup', 'form input[data-check-policy]', validatePassword);
131
	$('body').on('keyup', 'form input[data-check-equality]', passwordEquality);
132
	/*
133
	* Add event to handle displaying error/OK
134
	* while editing the first password
135
	* only if the second one is not empty
136
	*/
137
	$('input[data-check-equality]')
138
		.each(function () {
139
			var $input2 = $(this);
140
			$('body')
141
				.on('keyup', 'form input[type=password]:not([name=' + $input2.attr('name') + '])',
142
					function (event) {
143
						var $input1 = $(event.target);
144
						if ($input2.val().length) {
145
							displayPasswordEquality($input2, $input1);
146
						}
147
					});
148
		});
149
	/* add the a2-password-show-button after the first input */
150
	$('input[data-show-all]')
151
		.each(function () {
152
			var $this = $(this);
153
			if (!$('#a2-password-show-button-' + $this.attr('name')).length) {
154
				$(this).after($('<i class="a2-password-show-button" id="a2-password-show-button-'
155
					+ $this.attr('name') + '"></i>')
156
						.on('mousedown', showPassword)
157
						.on('mouseup mouseleave', hidePassword)
158
				);
159
			}
160
		});
161
	/* show the last character on keypress */
162
	$('input[data-show-last]')
163
		.each(function () {
164
			var $this = $(this);
165
			if (!$('#a2-password-show-last-' + $this.attr('name')).length) {
166
				// on crée un div placé dans le padding-right de l'input
167
				var $span = $('<span class="a2-password-show-last" id="a2-password-show-last-'
168
					+ $this.attr('name') + '"></span>)')
169
				$span.css({
170
					'font-size': $this.css('font-size'),
171
					'font-family': $this.css('font-family'),
172
					'line-height': parseInt($this.css('line-height').replace('px', '')) - parseInt($this.css('padding-bottom').replace('px', '')) + 'px',
173
					'vertical-align': $this.css('vertical-align'),
174
					'padding-top': $this.css('padding-top'),
175
					'padding-bottom': $this.css('padding-bottom')
176
				});
177
				$this.after($span);
178
			}
179
		});
180
	$('body').on('keyup', 'form input[data-show-last]', showLastChar);
181
});
src/authentic2/templates/authentic2/widgets/assisted_password.html
1
{% load i18n %}
2
<input class="a2-password-assisted" {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "authentic2/widgets/attrs.html" %}>
3
{% if features.check_policy %}
4
<p class="a2-password-policy-intro">{% blocktrans %}In order to create a secure password, please use <i>at least</i> : {% endblocktrans %}</p>
5
<ul class="a2-password-policy-helper" id="a2-password-policy-helper-{{ widget.attrs.name }}">
6
	{% comment %}Required to display the initial rules on page load{% endcomment %}
7
	{% if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH %}
8
	<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% blocktrans with A2_PASSWORD_POLICY_MIN_LENGTH=app_settings.A2_PASSWORD_POLICY_MIN_LENGTH %}{{ A2_PASSWORD_POLICY_MIN_LENGTH }} characters{% endblocktrans %}</li>
9
	{% endif %}
10
	{% if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 0 %}
11
	<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% trans "1 lowercase letter" %}</li>
12
	{% endif %}
13
	{% if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 1 %}
14
	<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% trans "1 digit" %}</li>
15
	{% endif %}
16
	{% if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 2 %}
17
	<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% trans "1 uppercase letter" %}</li>
18
	{% endif %}
19
	{% if app_settings.A2_PASSWORD_POLICY_REGEX %}
20
		{% if app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG %}
21
			<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% blocktrans with A2_PASSWORD_POLICY_REGEX_ERROR_MSG=app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG %}{{ A2_PASSWORD_POLICY_REGEX_ERROR_MSG }}{% endblocktrans %}</li>
22
		{% else %}
23
			<li class="a2-password-policy-rule"><i class="a2-password-icon"></i>{% blocktrans with A2_PASSWORD_POLICY_REGEX=app_settings.A2_PASSWORD_POLICY_REGEX %}Match the regular expression: {{ A2_PASSWORD_POLICY_REGEX }}, please change this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.'{% endblocktrans %}</li>
24
		{% endif %}
25
	{% endif %}
26
</ul>
27
{% endif %}
28
{% if features.check_equality %}
29
<ul class="a2-passwords-messages" id="a2-password-equality-helper-{{ widget.attrs.name }}">
30
	<li class="a2-passwords-default"><i class="a2-password-icon"></i>{% trans 'Both passwords must match.' %}</li>
31
	<li class="a2-passwords-matched"><i class="a2-password-icon"></i>{% trans 'Passwords match.' %}</li>
32
	<li class="a2-passwords-unmatched"><i class="a2-password-icon"></i>{% trans 'Passwords do not match.' %}</li>
33
</ul>
34
{% endif %}
src/authentic2/templates/authentic2/widgets/attrs.html
1
{% comment %}Will be deprecated in Django 1.11 : replace with django/forms/widgets/attrs.html{% endcomment %}
2
{% for name, value in widget.attrs.items %}{% if value != False %} {{ name }}{% if value != True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %}
src/authentic2/templates/registration/registration_completion_form.html
25 25
{% block content %}
26 26
      <h2>{% trans "Registration" %}</h2>
27 27
      <p>{% trans "Please fill the form to complete your registration" %}</p>
28
      <form method="post">
28
      <form method="post" class="a2-registration-completion">
29 29
        {% csrf_token %}
30 30
        {{ form.as_p }}
31 31
        <button class="submit-button">{% trans 'Submit' %}</button>
tests/test_api.py
834 834
            ('x' * 8,        False,  True,  True, False, False),
835 835
            ('x' * 8 + '1',  False,  True,  True,  True, False),
836 836
            ('x' * 8 + '1X',  True,  True,  True,  True,  True)):
837
        response = app.post_json('/api/validate-password/', params={'password': password}) 
837
        response = app.post_json('/api/validate-password/', params={'password': password})
838 838
        assert response.json['result'] == 1
839 839
        assert response.json['ok'] is ok
840 840
        assert len(response.json['checks']) == 4
tests/test_registration.py
1 1
# -*- coding: utf-8 -*-
2 2

  
3
import re
3 4
from urlparse import urlparse
4 5

  
5 6
from django.core.urlresolvers import reverse
......
585 586
    response = response.form.submit()
586 587
    assert new_next_url in response.content
587 588

  
589

  
590
def test_registration_activate_passwords_not_equal(app, db, settings, mailoutbox):
591
    settings.LANGUAGE_CODE = 'en-us'
592
    settings.A2_VALIDATE_EMAIL_DOMAIN = can_resolve_dns()
593
    settings.A2_EMAIL_IS_UNIQUE = True
594

  
595
    response = app.get(reverse('registration_register'))
596
    response.form.set('email', 'testbot@entrouvert.com')
597
    response = response.form.submit()
598
    response = response.follow()
599
    link = get_link_from_mail(mailoutbox[0])
600
    response = app.get(link)
601
    response.form.set('password1', 'azerty12AZ')
602
    response.form.set('password2', 'AAAazerty12AZ')
603
    response = response.form.submit()
604
    assert "The two password fields didn&#39;t match." in response.content
605

  
606

  
607
def test_registration_activate_assisted_password(app, db, settings, mailoutbox):
608
    response = app.get(reverse('registration_register'))
609
    response.form.set('email', 'testbot@entrouvert.com')
610
    response = response.form.submit()
611
    response = response.follow()
612
    link = get_link_from_mail(mailoutbox[0])
613
    response = app.get(link)
614
    # check presence of the script and css for RegistrationCompletionForm to work
615
    assert "password.js" in response.content
616
    assert "password.css" in response.content
617
    # check default attributes for password.js and css to work
618
    assert re.search('<input class="a2-password-assisted".*data-show-last.*>', response.content, re.I | re.M | re.S)
619
    assert re.search('<input class="a2-password-assisted".*data-check-equality.*>', response.content, re.I | re.M | re.S)
620
    assert re.search('<input class="a2-password-assisted".*data-check-policy.*>', response.content, re.I | re.M | re.S)
621
    # check template containers for password.js to display its results
622
    assert re.search('class="a2-passwords-messages" id="a2-password-equality-helper-', response.content, re.I | re.M | re.S)
623
    assert re.search('class="a2-password-policy-helper" id="a2-password-policy-helper-', response.content, re.I | re.M | re.S)
624
    assert re.search('class="a2-password-policy-rule"', response.content, re.I | re.M | re.S)
625

  
626

  
627
def test_registration_activate_password_no_show_all_button(app, db, settings, mailoutbox):
628
    response = app.get(reverse('registration_register'))
629
    response.form.set('email', 'testbot@entrouvert.com')
630
    response = response.form.submit()
631
    response = response.follow()
632
    link = get_link_from_mail(mailoutbox[0])
633
    response = app.get(link)
634
    assert not re.search('<input class="a2-password-assisted".*data-show-all.*>', response.content, re.I | re.M | re.S)
588
-