1from plain import forms
2from plain.auth import get_user_model
3from plain.exceptions import ValidationError
4from plain.models.forms import ModelForm
5
6from .core import check_user_password
7from .hashers import check_password
8from .utils import unicode_ci_compare
9
10
11class PasswordResetForm(forms.Form):
12 email = forms.EmailField(max_length=254)
13
14 def send_mail(
15 self,
16 *,
17 template_name: str,
18 context: dict,
19 from_email: str,
20 to_email: str,
21 ):
22 from plain.email import TemplateEmail
23
24 email = TemplateEmail(
25 template=template_name,
26 context=context,
27 from_email=from_email,
28 to=[to_email],
29 headers={
30 "X-Auto-Response-Suppress": "All",
31 },
32 )
33
34 email.send()
35
36 def get_users(self, email):
37 """Given an email, return matching user(s) who should receive a reset.
38
39 This allows subclasses to more easily customize the default policies
40 that prevent inactive users and users with unusable passwords from
41 resetting their password.
42 """
43 active_users = get_user_model()._default_manager.filter(email__iexact=email)
44 return (u for u in active_users if unicode_ci_compare(email, u.email))
45
46 def save(
47 self,
48 *,
49 generate_reset_url: callable,
50 email_template_name: str = "password_reset",
51 from_email: str = "",
52 extra_email_context: dict | None = None,
53 ):
54 """
55 Generate a one-use only link for resetting password and send it to the
56 user.
57 """
58 email = self.cleaned_data["email"]
59 for user in self.get_users(email):
60 context = {
61 "email": email,
62 "user": user,
63 "url": generate_reset_url(user),
64 **(extra_email_context or {}),
65 }
66 self.send_mail(
67 template_name=email_template_name,
68 context=context,
69 from_email=from_email,
70 to_email=user.email,
71 )
72
73
74class PasswordSetForm(forms.Form):
75 """
76 A form that lets a user set their password without entering the old
77 password
78 """
79
80 new_password1 = forms.CharField(strip=False)
81 new_password2 = forms.CharField(strip=False)
82
83 def __init__(self, user, *args, **kwargs):
84 self.user = user
85 super().__init__(*args, **kwargs)
86
87 def clean_new_password2(self):
88 password1 = self.cleaned_data.get("new_password1")
89 password2 = self.cleaned_data.get("new_password2")
90 if password1 and password2 and password1 != password2:
91 raise ValidationError(
92 "The two password fields didn't match.",
93 code="password_mismatch",
94 )
95
96 # Clean it as if it were being put into the model directly
97 self.user._meta.get_field("password").clean(password2, self.user)
98
99 return password2
100
101 def save(self, commit=True):
102 self.user.password = self.cleaned_data["new_password1"]
103 if commit:
104 self.user.save()
105 return self.user
106
107
108class PasswordChangeForm(PasswordSetForm):
109 """
110 A form that lets a user change their password by entering their old
111 password.
112 """
113
114 current_password = forms.CharField(strip=False)
115
116 def clean_current_password(self):
117 """
118 Validate that the current_password field is correct.
119 """
120 current_password = self.cleaned_data["current_password"]
121 if not check_user_password(self.user, current_password):
122 raise ValidationError(
123 "Your old password was entered incorrectly. Please enter it again.",
124 code="password_incorrect",
125 )
126 return current_password
127
128
129class PasswordLoginForm(forms.Form):
130 email = forms.EmailField(max_length=150)
131 password = forms.CharField(strip=False)
132
133 def clean(self):
134 User = get_user_model()
135
136 email = self.cleaned_data.get("email")
137 password = self.cleaned_data.get("password")
138
139 if email and password:
140 try:
141 # The vast majority of users won't have a case-sensitive email, so we act that way
142 user = User.objects.get(email__iexact=email)
143 except User.DoesNotExist:
144 # Run the default password hasher once to reduce the timing
145 # difference between an existing and a nonexistent user (django #20760).
146 check_password(password, "")
147
148 raise ValidationError(
149 "Please enter a correct email and password. Note that both fields may be case-sensitive.",
150 code="invalid_login",
151 )
152
153 if not check_user_password(user, password):
154 raise ValidationError(
155 "Please enter a correct email and password. Note that both fields may be case-sensitive.",
156 code="invalid_login",
157 )
158
159 self._user = user
160
161 return self.cleaned_data
162
163 def get_user(self):
164 return self._user
165
166
167class PasswordSignupForm(ModelForm):
168 confirm_password = forms.CharField(strip=False)
169
170 class Meta:
171 model = get_user_model()
172 fields = ("email", "password")
173
174 def clean(self):
175 cleaned_data = super().clean()
176 password = cleaned_data.get("password")
177 confirm_password = cleaned_data.get("confirm_password")
178 if password and confirm_password and password != confirm_password:
179 raise ValidationError(
180 "The two password fields didn't match.",
181 code="password_mismatch",
182 )
183 return cleaned_data