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 |
|
-
|