Added PasswordResetView.
This commit is contained in:
parent
f48410024e
commit
e03d715602
@ -18,7 +18,8 @@ Core:
|
|||||||
mode [#3799, #3817].
|
mode [#3799, #3817].
|
||||||
- Changed format for elements send via autoupdate [#3926].
|
- Changed format for elements send via autoupdate [#3926].
|
||||||
- Add a change-id system to get only new elements [#3938].
|
- Add a change-id system to get only new elements [#3938].
|
||||||
- Switch from Yarn back to npm.
|
- Switch from Yarn back to npm [#3964].
|
||||||
|
- Added password reset link (password reset via email) [#3914].
|
||||||
|
|
||||||
Motions:
|
Motions:
|
||||||
- Option to customly sort motions [#3894].
|
- Option to customly sort motions [#3894].
|
||||||
|
@ -20,4 +20,12 @@ urlpatterns = [
|
|||||||
url(r'^setpassword/$',
|
url(r'^setpassword/$',
|
||||||
views.SetPasswordView.as_view(),
|
views.SetPasswordView.as_view(),
|
||||||
name='user_setpassword'),
|
name='user_setpassword'),
|
||||||
|
|
||||||
|
url(r'^reset-password/$',
|
||||||
|
views.PasswordResetView.as_view(),
|
||||||
|
name='user_reset_password'),
|
||||||
|
|
||||||
|
url(r'^reset-password-confirm/$',
|
||||||
|
views.PasswordResetConfirmView.as_view(),
|
||||||
|
name='password_reset_confirm'),
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import smtplib
|
import smtplib
|
||||||
|
import textwrap
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
@ -10,11 +11,15 @@ from django.contrib.auth import (
|
|||||||
)
|
)
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
from django.contrib.auth.password_validation import validate_password
|
from django.contrib.auth.password_validation import validate_password
|
||||||
|
from django.contrib.auth.tokens import default_token_generator
|
||||||
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_bytes, force_text
|
||||||
|
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.template import loader
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..core.signals import permission_change
|
from ..core.signals import permission_change
|
||||||
@ -524,3 +529,113 @@ class SetPasswordView(APIView):
|
|||||||
else:
|
else:
|
||||||
raise ValidationError({'detail': _('Old password does not match.')})
|
raise ValidationError({'detail': _('Old password does not match.')})
|
||||||
return super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetView(APIView):
|
||||||
|
"""
|
||||||
|
Users can send an email to themselves to get a password reset email.
|
||||||
|
|
||||||
|
Send POST request with {'email': <email addresss>} and all users with this
|
||||||
|
address will receive an email (means Django sends one or more emails to
|
||||||
|
this address) with a one-use only link.
|
||||||
|
"""
|
||||||
|
http_method_names = ['post']
|
||||||
|
use_https = False #TODO: Do we use https?
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Loop over all users and send emails.
|
||||||
|
"""
|
||||||
|
to_email = request.data.get('email')
|
||||||
|
for user in self.get_users(to_email):
|
||||||
|
current_site = get_current_site(request)
|
||||||
|
site_name = current_site.name
|
||||||
|
context = {
|
||||||
|
'email': to_email,
|
||||||
|
'site_name': site_name,
|
||||||
|
'protocol': 'https' if self.use_https else 'http',
|
||||||
|
'domain': current_site.domain,
|
||||||
|
'path': '/reset-password-confirm/',
|
||||||
|
'user_id': urlsafe_base64_encode(force_bytes(user.pk)).decode(),
|
||||||
|
'token': default_token_generator.make_token(user),
|
||||||
|
'username': user.get_username(),
|
||||||
|
}
|
||||||
|
# Send a django.core.mail.EmailMessage to `to_email`.
|
||||||
|
subject = _('Password reset for {}').format(site_name)
|
||||||
|
subject = ''.join(subject.splitlines())
|
||||||
|
body = self.get_email_body(**context)
|
||||||
|
from_email = None # TODO: Add nice from_email here.
|
||||||
|
email_message = mail.EmailMessage(subject, body, from_email, [to_email])
|
||||||
|
email_message.send()
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_users(self, email):
|
||||||
|
"""Given an email, return matching user(s) who should receive a reset.
|
||||||
|
|
||||||
|
This allows subclasses to more easily customize the default policies
|
||||||
|
that prevent inactive users and users with unusable passwords from
|
||||||
|
resetting their password.
|
||||||
|
"""
|
||||||
|
active_users = User.objects.filter(**{
|
||||||
|
'email__iexact': email,
|
||||||
|
'is_active': True,
|
||||||
|
})
|
||||||
|
return (u for u in active_users if u.has_usable_password())
|
||||||
|
|
||||||
|
def get_email_body(self, **context):
|
||||||
|
"""
|
||||||
|
Add context to email template and return the complete body.
|
||||||
|
"""
|
||||||
|
return textwrap.dedent(
|
||||||
|
"""
|
||||||
|
You're receiving this email because you requested a password reset for your user account at {site_name}.
|
||||||
|
|
||||||
|
Please go to the following page and choose a new password:
|
||||||
|
|
||||||
|
{protocol}://{domain}{path}?user_id={user_id}&token={token}
|
||||||
|
|
||||||
|
Your username, in case you've forgotten: {username}
|
||||||
|
|
||||||
|
Thanks for using our site!
|
||||||
|
|
||||||
|
The {site_name} team.
|
||||||
|
"""
|
||||||
|
).format(**context)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetConfirmView(APIView):
|
||||||
|
"""
|
||||||
|
View to reset the password.
|
||||||
|
|
||||||
|
Send POST request with {'user_id': <encoded user id>, 'token': <token>,
|
||||||
|
'password' <new password>} to set password of this user to the new one.
|
||||||
|
"""
|
||||||
|
http_method_names = ['post']
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
uidb64 = request.data.get('user_id')
|
||||||
|
token = request.data.get('token')
|
||||||
|
password = request.data.get('password')
|
||||||
|
if not (uidb64 and token and password):
|
||||||
|
raise ValidationError({'detail': _('You have to provide user_id, token and password.')})
|
||||||
|
user = self.get_user(uidb64)
|
||||||
|
if user is None:
|
||||||
|
raise ValidationError({'detail': _('User does not exist.')})
|
||||||
|
if not default_token_generator.check_token(user, token):
|
||||||
|
raise ValidationError({'detail': _('Invalid token.')})
|
||||||
|
try:
|
||||||
|
validate_password(password, user=user)
|
||||||
|
except DjangoValidationError as errors:
|
||||||
|
raise ValidationError({'detail': ' '.join(errors)})
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_user(self, uidb64):
|
||||||
|
try:
|
||||||
|
# urlsafe_base64_decode() decodes to bytestring
|
||||||
|
uid = urlsafe_base64_decode(uidb64).decode()
|
||||||
|
user = User.objects.get(pk=uid)
|
||||||
|
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
|
||||||
|
user = None
|
||||||
|
return user
|
||||||
|
Loading…
Reference in New Issue
Block a user