From 1b3eececf2ee43d5ec4a622ccd04435a9211f60f Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 2 Sep 2019 15:44:37 +0300 Subject: [PATCH] refactored authorization and account app --- apps/account/models.py | 18 +---- apps/account/serializers/common.py | 85 ------------------------ apps/account/serializers/web.py | 83 +++++++++++++++++++++++ apps/account/urls/common.py | 3 - apps/account/urls/web.py | 11 +-- apps/account/views/common.py | 63 ------------------ apps/account/views/web.py | 57 +++++++++++++++- apps/authorization/serializers/common.py | 12 ++-- 8 files changed, 153 insertions(+), 179 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index da1711bd..497fd188 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -106,22 +106,8 @@ class User(ImageMixin, AbstractUser): self.is_active = True self.save() - def remove_access_tokens(self, source: Union[int, Union[tuple, list]]): - """Method to remove user access tokens""" - source = source if isinstance(source, list) else [source, ] - self.oauth2_provider_accesstoken.filter(application__source__in=source)\ - .delete() - - def revoke_refresh_tokens(self, source: Union[int, tuple, list]): - """Method to remove user refresh tokens""" - source = source if isinstance(source, list) else [source, ] - refresh_tokens = self.oauth2_provider_refreshtoken.filter( - application__source__in=source, - access_token__isnull=False - ) - if refresh_tokens.exists(): - for token in refresh_tokens: - token.revoke() + def revoke_access_token(self): + print('Revoke token') @property def confirm_email_token(self): diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 2d293c68..f2dae3c2 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -3,10 +3,6 @@ from fcm_django.models import FCMDevice from rest_framework import serializers, exceptions from account import models -from utils import exceptions as utils_exceptions -from rest_framework_simplejwt import tokens -from django.conf import settings -from account import tasks # User serializers @@ -62,84 +58,3 @@ class FCMDeviceSerializer(serializers.ModelSerializer): instance.user = None instance.save() return instance - - -class RefreshTokenSerializer(serializers.Serializer): - """Serializer for refresh token view""" - refresh_token = serializers.CharField(read_only=True) - access_token = serializers.CharField(read_only=True) - - def get_request(self): - """Return request""" - return self.context.get('request') - - def validate(self, attrs): - """Override validate method""" - refresh_token = self.get_request().COOKIES.get('refresh_token') - if not refresh_token: - raise utils_exceptions.NotValidRefreshTokenError() - - token = tokens.RefreshToken(token=refresh_token) - - data = {'access_token': str(token.access_token)} - - if settings.SIMPLE_JWT.get('ROTATE_REFRESH_TOKENS'): - if settings.SIMPLE_JWT.get('BLACKLIST_AFTER_ROTATION'): - try: - # Attempt to blacklist the given refresh token - token.blacklist() - except AttributeError: - # If blacklist app not installed, `blacklist` method will - # not be present - pass - - token.set_jti() - token.set_exp() - - data['refresh_token'] = str(token) - - return data - - -class ChangeEmailSerializer(serializers.ModelSerializer): - """Change user email serializer""" - - class Meta: - """Meta class""" - model = models.User - fields = ( - 'id', - 'email', - ) - read_only_fields = ( - 'id', - ) - - def validate_email(self, value): - """Validate email value""" - if value == self.instance.email: - # todo: add custom exception - raise serializers.ValidationError() - return value - - def validate(self, attrs): - """Override validate method""" - email_confirmed = self.instance.email_confirmed - if not email_confirmed: - # todo: add custom exception - raise serializers.ValidationError() - return attrs - - def update(self, instance, validated_data): - """ - Override update method - """ - instance.email = validated_data.get('email') - instance.email_confirmed = False - instance.save() - # Send verification link on user email for change email address - if settings.USE_CELERY: - tasks.confirm_new_email_address.delay(instance.id) - else: - tasks.confirm_new_email_address(instance.id) - return instance diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index 8d5f1cd9..4b9b87f8 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -1,6 +1,7 @@ """Serializers for account web""" from django.conf import settings from django.contrib.auth import password_validation as password_validators +from rest_framework_simplejwt import tokens from django.db.models import Q from rest_framework import serializers @@ -121,3 +122,85 @@ class ChangePasswordSerializer(serializers.ModelSerializer): instance.set_password(validated_data.get('password')) instance.save() return instance + + +class ChangeEmailSerializer(serializers.ModelSerializer): + """Change user email serializer""" + + class Meta: + """Meta class""" + model = models.User + fields = ( + 'id', + 'email', + ) + read_only_fields = ( + 'id', + ) + + def validate_email(self, value): + """Validate email value""" + if value == self.instance.email: + # todo: add custom exception + raise serializers.ValidationError() + return value + + def validate(self, attrs): + """Override validate method""" + email_confirmed = self.instance.email_confirmed + if not email_confirmed: + # todo: add custom exception + raise serializers.ValidationError() + return attrs + + def update(self, instance, validated_data): + """ + Override update method + """ + instance.email = validated_data.get('email') + instance.email_confirmed = False + instance.save() + # Send verification link on user email for change email address + if settings.USE_CELERY: + tasks.confirm_new_email_address.delay(instance.id) + else: + tasks.confirm_new_email_address(instance.id) + instance.revoke_access_token() + return instance + + +class RefreshTokenSerializer(serializers.Serializer): + """Serializer for refresh token view""" + refresh_token = serializers.CharField(read_only=True) + access_token = serializers.CharField(read_only=True) + + def get_request(self): + """Return request""" + return self.context.get('request') + + def validate(self, attrs): + """Override validate method""" + refresh_token = self.get_request().COOKIES.get('refresh_token') + if not refresh_token: + raise utils_exceptions.NotValidRefreshTokenError() + + token = tokens.RefreshToken(token=refresh_token) + + data = {'access_token': str(token.access_token)} + + if settings.SIMPLE_JWT.get('ROTATE_REFRESH_TOKENS'): + if settings.SIMPLE_JWT.get('BLACKLIST_AFTER_ROTATION'): + try: + # Attempt to blacklist the given refresh token + token.blacklist() + except AttributeError: + # If blacklist app not installed, `blacklist` method will + # not be present + pass + + token.set_jti() + token.set_exp() + + data['refresh_token'] = str(token) + + return data diff --git a/apps/account/urls/common.py b/apps/account/urls/common.py index 2a426fcb..6a909588 100644 --- a/apps/account/urls/common.py +++ b/apps/account/urls/common.py @@ -7,7 +7,4 @@ app_name = 'account' urlpatterns = [ path('user/', views.UserView.as_view(), name='user-get-update'), - path('refresh-token/', views.RefreshTokenView.as_view(), name='refresh-token'), - path('change-email/', views.ChangeEmailView.as_view(), name='change-email'), - path('change-email/confirm///', views.ChangeEmailConfirmView.as_view(), name='change-email-confirm'), ] diff --git a/apps/account/urls/web.py b/apps/account/urls/web.py index 89612ded..5f4bdf45 100644 --- a/apps/account/urls/web.py +++ b/apps/account/urls/web.py @@ -7,15 +7,16 @@ from account.views import web as views app_name = 'account' urlpatterns_api = [ - path('change-password/', views.ChangePasswordView.as_view(), - name='change-password'), - path('reset-password/', views.PasswordResetView.as_view(), - name='password-reset'), + path('change-password/', views.ChangePasswordView.as_view(), name='change-password'), + path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'), path('form/reset-password///', views.FormPasswordResetConfirmView.as_view(), name='form-password-reset-confirm'), - # Redirect endpoint after success path('form/reset-password/success/', views.FormPasswordResetSuccessView.as_view(), name='form-password-reset-success'), + path('refresh-token/', views.RefreshTokenView.as_view(), name='refresh-token'), + path('change-email/', views.ChangeEmailView.as_view(), name='change-email'), + path('change-email/confirm///', views.ChangeEmailConfirmView.as_view(), + name='change-email-confirm'), ] urlpatterns = urlpatterns_api + \ diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 15abde77..d7a422a2 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -3,14 +3,9 @@ from fcm_django.models import FCMDevice from rest_framework import generics, status from rest_framework import permissions from rest_framework.response import Response -from django.utils.encoding import force_text -from django.utils.http import urlsafe_base64_decode -from utils.models import GMTokenGenerator -from utils import exceptions as utils_exceptions from account import models from account.serializers import common as serializers -from utils.views import JWTGenericViewMixin # User views @@ -58,61 +53,3 @@ class FCMDeviceViewSet(generics.GenericAPIView): obj = queryset.filter(**filter).first() obj and self.check_object_permissions(self.request, obj) return obj - - -# Refresh access_token -class RefreshTokenView(JWTGenericViewMixin): - """Refresh access_token""" - permission_classes = (permissions.IsAuthenticated,) - serializer_class = serializers.RefreshTokenSerializer - - def post(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - response = Response(serializer.data, status=status.HTTP_201_CREATED) - access_token = serializer.data.get('access_token') - refresh_token = serializer.data.get('refresh_token') - return self._put_cookies_in_response( - cookies=self._put_data_in_cookies(access_token=access_token, - refresh_token=refresh_token), - response=response) - - -# Change user email -class ChangeEmailView(JWTGenericViewMixin): - """Change user email view""" - serializer_class = serializers.ChangeEmailSerializer - queryset = models.User.objects.all() - - def patch(self, request, *args, **kwargs): - """Implement POST-method""" - # Get user instance - instance = self.request.user - - serializer = self.get_serializer(data=request.data, instance=instance) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(status=status.HTTP_200_OK) - - -class ChangeEmailConfirmView(JWTGenericViewMixin): - """View for confirm changing email""" - - permission_classes = (permissions.AllowAny, ) - - def get(self, request, *args, **kwargs): - """Implement GET-method""" - uidb64 = kwargs.get('uidb64') - token = kwargs.get('token') - uid = force_text(urlsafe_base64_decode(uidb64)) - user_qs = models.User.objects.filter(pk=uid) - if user_qs.exists(): - user = user_qs.first() - if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( - user, token): - raise utils_exceptions.NotValidTokenError() - # Approve email status - user.confirm_email() - return Response(status=status.HTTP_200_OK) - else: - raise utils_exceptions.UserNotFoundError() diff --git a/apps/account/views/web.py b/apps/account/views/web.py index 591b5622..10afe18d 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -27,7 +27,6 @@ from utils.views import (JWTCreateAPIView, JWTGenericViewMixin) -# Password reset class PasswordResetView(JWTCreateAPIView): """View for resetting user password""" permission_classes = (permissions.AllowAny, ) @@ -89,6 +88,62 @@ class ChangePasswordView(JWTUpdateAPIView): return Response(status=status.HTTP_200_OK) +class ChangeEmailView(JWTGenericViewMixin): + """Change user email view""" + serializer_class = serializers.ChangeEmailSerializer + queryset = models.User.objects.all() + + def patch(self, request, *args, **kwargs): + """Implement POST-method""" + # Get user instance + instance = self.request.user + + serializer = self.get_serializer(data=request.data, instance=instance) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(status=status.HTTP_200_OK) + + +class ChangeEmailConfirmView(JWTGenericViewMixin): + """View for confirm changing email""" + + permission_classes = (permissions.AllowAny,) + + def get(self, request, *args, **kwargs): + """Implement GET-method""" + uidb64 = kwargs.get('uidb64') + token = kwargs.get('token') + uid = force_text(urlsafe_base64_decode(uidb64)) + user_qs = models.User.objects.filter(pk=uid) + if user_qs.exists(): + user = user_qs.first() + if not GMTokenGenerator(GMTokenGenerator.CHANGE_EMAIL).check_token( + user, token): + raise utils_exceptions.NotValidTokenError() + # Approve email status + user.confirm_email() + return Response(status=status.HTTP_200_OK) + else: + raise utils_exceptions.UserNotFoundError() + + +class RefreshTokenView(JWTGenericViewMixin): + """Refresh access_token""" + permission_classes = (permissions.IsAuthenticated,) + serializer_class = serializers.RefreshTokenSerializer + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + response = Response(serializer.data, status=status.HTTP_201_CREATED) + access_token = serializer.data.get('access_token') + refresh_token = serializer.data.get('refresh_token') + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies(access_token=access_token, + refresh_token=refresh_token), + response=response) + + # Form view class PasswordContextMixin: extra_context = None diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index f7750468..d889ad78 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -161,8 +161,8 @@ class LogoutSerializer(serializers.ModelSerializer): 'jti' ) - def validate(self, attrs): - """Override validated data""" + def create(self, validated_data): + """Override create method""" request = self.context.get('request') # Get token bytes from cookies (result: b'Bearer ') token_bytes = utils_methods.get_token_from_cookies(request) @@ -171,10 +171,10 @@ class LogoutSerializer(serializers.ModelSerializer): # Get access token obj access_token = tokens.AccessToken(token) # Prepare validated data - attrs['user'] = request.user - attrs['token'] = access_token.token - attrs['jti'] = access_token.payload.get('jti') - return attrs + validated_data['user'] = request.user + validated_data['token'] = access_token.token + validated_data['jti'] = access_token.payload.get('jti') + return super().create(validated_data) # OAuth