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