Added PasswordResetView.

This commit is contained in:
Norman Jäckel 2018-10-09 22:00:55 +02:00
parent f48410024e
commit e03d715602
3 changed files with 126 additions and 2 deletions

View File

@ -18,7 +18,8 @@ Core:
mode [#3799, #3817].
- Changed format for elements send via autoupdate [#3926].
- 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:
- Option to customly sort motions [#3894].

View File

@ -20,4 +20,12 @@ urlpatterns = [
url(r'^setpassword/$',
views.SetPasswordView.as_view(),
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'),
]

View File

@ -1,4 +1,5 @@
import smtplib
import textwrap
from typing import List
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.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.exceptions import ValidationError as DjangoValidationError
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.template import loader
from ..core.config import config
from ..core.signals import permission_change
@ -524,3 +529,113 @@ class SetPasswordView(APIView):
else:
raise ValidationError({'detail': _('Old password does not match.')})
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