Projet

Général

Profil

0003-forms-passwords-add-phone-field-69890.patch

Paul Marillonnet, 17 janvier 2023 15:49

Télécharger (7,52 ko)

Voir les différences:

Subject: [PATCH 3/5] forms/passwords: add phone field (#69890)

 src/authentic2/forms/passwords.py | 93 ++++++++++++++++++++++++++-----
 1 file changed, 78 insertions(+), 15 deletions(-)
src/authentic2/forms/passwords.py
20 20
from django import forms
21 21
from django.conf import settings
22 22
from django.contrib.auth import forms as auth_forms
23
from django.contrib.auth import get_user_model
23 24
from django.core.exceptions import ValidationError
24 25
from django.forms import Form
25 26
from django.utils.translation import gettext_lazy as _
......
32 33
from ..backends import get_user_queryset
33 34
from ..utils import hooks
34 35
from ..utils import misc as utils_misc
35
from .fields import CheckPasswordField, NewPasswordField, PasswordField, ValidatedEmailField
36
from ..utils import sms as utils_sms
37
from .fields import CheckPasswordField, NewPasswordField, PasswordField, PhoneField, ValidatedEmailField
36 38
from .honeypot import HoneypotForm
37 39
from .utils import NextUrlFormMixin
38 40

  
......
42 44
class PasswordResetForm(HoneypotForm):
43 45
    next_url = forms.CharField(widget=forms.HiddenInput, required=False)
44 46

  
45
    email = ValidatedEmailField(label=_("Email"))
47
    email = ValidatedEmailField(label=_("Email"), required=False)
48

  
49
    phone = PhoneField(
50
        label=_('Phone number'),
51
        help_text=_('Your mobile phone number.'),
52
        required=False,
53
    )
46 54

  
47 55
    def __init__(self, *args, **kwargs):
48 56
        super().__init__(*args, **kwargs)
49 57
        self.users = []
50 58
        if app_settings.A2_USER_CAN_RESET_PASSWORD_BY_USERNAME:
51 59
            del self.fields['email']
52
            self.fields['email_or_username'] = forms.CharField(label=_('Email or username'), max_length=254)
60
            self.fields['email_or_username'] = forms.CharField(
61
                label=_('Email or username'), max_length=254, required=False
62
            )
63

  
64
        if not app_settings.A2_ACCEPT_PHONE_AUTHENTICATION or not get_user_model()._meta.get_field('phone'):
65
            del self.fields['phone']
66
            if 'email' in self.fields:
67
                self.fields['email'].required = True
68
            else:
69
                self.fields['email_or_username'].required = True
53 70

  
54 71
    def clean_email(self):
55 72
        email = self.cleaned_data.get('email')
......
71 88
                self.cleaned_data['email'] = email_or_username
72 89
        return email_or_username
73 90

  
91
    def clean_phone(self):
92
        phone = self.cleaned_data.get('phone')
93
        if phone:
94
            self.users = get_user_queryset().filter(phone=phone)
95
            return phone
96

  
74 97
    def clean(self):
75
        if self.users and not any(user.email for user in self.users):
98
        if app_settings.A2_ACCEPT_PHONE_AUTHENTICATION and get_user_model()._meta.get_field('phone'):
99
            if (
100
                not self.cleaned_data['email']
101
                and not self.cleaned_data.get('email_or_username')
102
                and not self.cleaned_data['phone']
103
            ):
104
                raise ValidationError(_('Please provide an email address or a mobile phone number.'))
105
        elif self.users and not any(user.email for user in self.users):
76 106
            raise ValidationError(_('Your account has no email, you cannot ask for a password reset.'))
77 107
        return self.cleaned_data
78 108

  
79 109
    def save(self):
80 110
        """
81
        Generates a one-use only link for resetting password and sends to the
82
        user.
111
        Generates either:
112
        · a one-use only link for resetting password and sends to the user.
113
        · a code sent by SMS which the user needs to input in order to confirm password reset.
83 114
        """
84 115
        email = self.cleaned_data.get('email')
85 116
        email_or_username = self.cleaned_data.get('email_or_username')
117
        phone = self.cleaned_data.get('phone')
86 118

  
87 119
        active_users = self.users.filter(is_active=True)
88 120
        email_sent = False
121
        sms_sent = False
89 122

  
90 123
        for user in active_users:
91
            if not user.email:
92
                logger.info('password reset failed for account "%r": account has no email', user)
124
            if not user.email and not user.phone:
125
                logger.info(
126
                    'password reset failed for account "%r": account has no email nor mobile phone number',
127
                    user,
128
                )
93 129
                continue
94 130

  
95 131
            if user.userexternalid_set.exists():
......
116 152
            # we don't set the password to a random string, as some users should not have
117 153
            # a password
118 154
            set_random_password = user.has_usable_password() and app_settings.A2_SET_RANDOM_PASSWORD_ON_RESET
119
            email_sent = True
120
            utils_misc.send_password_reset_mail(
121
                user, set_random_password=set_random_password, next_url=self.cleaned_data.get('next_url')
122
            )
123 155
            journal.record('user.password.reset.request', email=user.email, user=user)
156
            if email or email_or_username:
157
                email_sent = True
158
                utils_misc.send_password_reset_mail(
159
                    user, set_random_password=set_random_password, next_url=self.cleaned_data.get('next_url')
160
                )
161
            elif phone:
162
                try:
163
                    sms_sent = True
164
                    code = utils_sms.send_password_reset_sms(
165
                        phone,
166
                        user.ou,
167
                        user=user,
168
                    )
169
                except utils_sms.SMSError:
170
                    pass
171
                else:
172
                    # all user info sending logic contained here, however the view needs to know
173
                    # which code was sent:
174
                    return (code, sms_sent)
175

  
124 176
        for user in self.users.filter(is_active=False):
125 177
            logger.info('password reset failed for user "%r": account is disabled', user)
126
            email_sent = True
127
            utils_misc.send_templated_mail(user, ['authentic2/password_reset_refused'])
178
            if email or email_or_username:
179
                email_sent = True
180
                code = utils_misc.send_templated_mail(user, ['authentic2/password_reset_refused'])
181
            elif phone:
182
                sms_sent = True
183
                # TODO send refusal notification SMS(?)
128 184
        if not email_sent and email:
129 185
            logger.info('password reset request for "%s", no user found', email)
130 186
            if getattr(settings, 'REGISTRATION_OPEN', True):
......
139 195
            else:
140 196
                ctx = {}
141 197
            utils_misc.send_templated_mail(email, ['authentic2/password_reset_no_account'], context=ctx)
142
        hooks.call_hooks('event', name='password-reset', email=email or email_or_username, users=active_users)
198
            hooks.call_hooks(
199
                'event', name='password-reset', email=email or email_or_username, users=active_users
200
            )
201
            return (None, email_sent)
202
        elif phone:
203
            # TODO hook(?)
204
            return (None, sms_sent)
205
        return (None, False)
143 206

  
144 207

  
145 208
class PasswordResetMixin(Form):
146
-