186 lines
6.7 KiB
Python
186 lines
6.7 KiB
Python
"""Web account views"""
|
|
from django.conf import settings
|
|
from django.contrib.auth.tokens import default_token_generator
|
|
from django.core.exceptions import ValidationError
|
|
from django.http import HttpResponseRedirect
|
|
from django.shortcuts import get_object_or_404
|
|
from django.urls import reverse_lazy
|
|
from django.utils.decorators import method_decorator
|
|
from django.utils.encoding import force_text
|
|
from django.utils.http import urlsafe_base64_decode
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.decorators.cache import never_cache
|
|
from django.views.decorators.debug import sensitive_post_parameters
|
|
from django.views.generic.edit import FormView
|
|
from rest_framework import permissions
|
|
from rest_framework import status
|
|
from rest_framework import views
|
|
from rest_framework.response import Response
|
|
|
|
from account import models
|
|
from account.forms import SetPasswordForm
|
|
from account.serializers import web as serializers
|
|
from utils import exceptions as utils_exceptions
|
|
from utils.models import GMTokenGenerator
|
|
from utils.views import JWTGenericViewMixin
|
|
|
|
|
|
class PasswordResetView(JWTGenericViewMixin):
|
|
"""View for resetting user password"""
|
|
permission_classes = (permissions.AllowAny, )
|
|
serializer_class = serializers.PasswordResetSerializer
|
|
queryset = models.ResetPasswordToken.objects.valid()
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
"""Override create method"""
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
if serializer.validated_data.get('user'):
|
|
serializer.save()
|
|
return Response(status=status.HTTP_200_OK)
|
|
|
|
|
|
class PasswordResetConfirmView(JWTGenericViewMixin):
|
|
"""View for confirmation new password"""
|
|
serializer_class = serializers.PasswordResetConfirmSerializer
|
|
permission_classes = (permissions.AllowAny,)
|
|
|
|
def get_queryset(self):
|
|
"""Override get_queryset method"""
|
|
return models.ResetPasswordToken.objects.valid()
|
|
|
|
def get_object(self):
|
|
"""Override get_object method
|
|
"""
|
|
queryset = self.filter_queryset(self.get_queryset())
|
|
uidb64 = self.kwargs.get('uidb64')
|
|
|
|
user_id = force_text(urlsafe_base64_decode(uidb64))
|
|
token = self.kwargs.get('token')
|
|
|
|
filter_kwargs = {'key': token, 'user_id': user_id}
|
|
obj = get_object_or_404(queryset, **filter_kwargs)
|
|
|
|
if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token(
|
|
user=obj.user, token=token):
|
|
raise utils_exceptions.NotValidAccessTokenError()
|
|
|
|
# May raise a permission denied
|
|
self.check_object_permissions(self.request, obj)
|
|
|
|
return obj
|
|
|
|
def put(self, request, *args, **kwargs):
|
|
"""Implement PUT method"""
|
|
instance = self.get_object()
|
|
serializer = self.get_serializer(instance=instance,
|
|
data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
serializer.save()
|
|
return Response(status=status.HTTP_200_OK)
|
|
|
|
|
|
# Form view
|
|
class PasswordContextMixin:
|
|
extra_context = None
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update({
|
|
'title': self.title,
|
|
**(self.extra_context or {})
|
|
})
|
|
return context
|
|
|
|
|
|
class FormPasswordResetSuccessView(views.APIView):
|
|
"""View for successful reset password"""
|
|
|
|
permission_classes = (permissions.AllowAny, )
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
"""Implement GET-method"""
|
|
return Response(status=status.HTTP_200_OK)
|
|
|
|
|
|
class FormPasswordResetConfirmView(PasswordContextMixin, FormView):
|
|
|
|
INTERNAL_RESET_URL_TOKEN = 'set-password'
|
|
INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token'
|
|
|
|
form_class = SetPasswordForm
|
|
post_reset_login = False
|
|
post_reset_login_backend = None
|
|
success_url = reverse_lazy('web:account:form-password-reset-success')
|
|
template_name = settings.CONFIRMATION_PASSWORD_RESET_TEMPLATE
|
|
title = _('Enter new password')
|
|
token_generator = default_token_generator
|
|
|
|
@method_decorator(sensitive_post_parameters())
|
|
@method_decorator(never_cache)
|
|
def dispatch(self, *args, **kwargs):
|
|
assert 'uidb64' in kwargs and 'token' in kwargs
|
|
|
|
self.validlink = False
|
|
self.user = self.get_user(kwargs['uidb64'])
|
|
|
|
if self.user is not None:
|
|
token = kwargs['token']
|
|
if token == self.INTERNAL_RESET_URL_TOKEN:
|
|
session_token = self.request.session.get(self.INTERNAL_RESET_SESSION_TOKEN)
|
|
if self.token_generator.check_token(self.user, session_token):
|
|
# If the token is valid, display the password reset form.
|
|
self.validlink = True
|
|
return super().dispatch(*args, **kwargs)
|
|
else:
|
|
if self.token_generator.check_token(self.user, token):
|
|
# Store the token in the session and redirect to the
|
|
# password reset form at a URL without the token. That
|
|
# avoids the possibility of leaking the token in the
|
|
# HTTP Referer header.
|
|
self.request.session[self.INTERNAL_RESET_SESSION_TOKEN] = token
|
|
redirect_url = self.request.path.replace(token, self.INTERNAL_RESET_URL_TOKEN)
|
|
return HttpResponseRedirect(redirect_url)
|
|
|
|
# Display the "Password reset unsuccessful" page.
|
|
return self.render_to_response(self.get_context_data())
|
|
|
|
def get_user(self, uidb64):
|
|
try:
|
|
# urlsafe_base64_decode() decodes to bytestring
|
|
uid = urlsafe_base64_decode(uidb64).decode()
|
|
user = models.User.objects.get(pk=uid)
|
|
except (TypeError, ValueError, OverflowError, models.User.DoesNotExist, ValidationError):
|
|
user = None
|
|
return user
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['user'] = self.user
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
# Saving form
|
|
form.save()
|
|
user = form.user
|
|
|
|
# Expire user tokens
|
|
user.expire_access_tokens()
|
|
user.expire_refresh_tokens()
|
|
|
|
# Pop session token
|
|
del self.request.session[self.INTERNAL_RESET_SESSION_TOKEN]
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
if self.validlink:
|
|
context['validlink'] = True
|
|
else:
|
|
context.update({
|
|
'form': None,
|
|
'title': _('Password reset unsuccessful'),
|
|
'validlink': False,
|
|
})
|
|
return context
|