From defe6abf4cd9e86b2f8b7be4367154769aead9a1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 5 Sep 2019 17:19:10 +0300 Subject: [PATCH] refactored account and authorization endpoints --- apps/account/admin.py | 5 +- apps/account/serializers/common.py | 147 ++++++++++++++++++++++- apps/account/serializers/web.py | 144 ---------------------- apps/account/urls/common.py | 9 +- apps/account/urls/web.py | 8 -- apps/account/views/common.py | 94 ++++++++++++++- apps/account/views/web.py | 105 ---------------- apps/authorization/serializers/common.py | 40 ++++++ apps/authorization/urls/common.py | 3 +- apps/authorization/views/common.py | 20 +++ 10 files changed, 306 insertions(+), 269 deletions(-) diff --git a/apps/account/admin.py b/apps/account/admin.py index e9c853bb..938be965 100644 --- a/apps/account/admin.py +++ b/apps/account/admin.py @@ -11,8 +11,9 @@ class UserAdmin(BaseUserAdmin): """User model admin settings.""" list_display = ('id', 'username', 'short_name', 'date_joined', 'is_active', - 'is_staff', 'is_superuser',) - list_filter = ('is_active', 'is_staff', 'is_superuser', 'groups',) + 'is_staff', 'is_superuser', 'email_confirmed') + list_filter = ('is_active', 'is_staff', 'is_superuser', 'email_confirmed', + 'groups',) search_fields = ('email', 'first_name', 'last_name') readonly_fields = ('last_login', 'date_joined',) fieldsets = ( diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index f2dae3c2..4a942c52 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -1,20 +1,159 @@ """Common account serializers""" +from django.conf import settings +from django.contrib.auth import password_validation as password_validators from fcm_django.models import FCMDevice -from rest_framework import serializers, exceptions +from rest_framework import exceptions +from rest_framework import serializers -from account import models +from account import models, tasks +from utils import exceptions as utils_exceptions # User serializers class UserSerializer(serializers.ModelSerializer): """User serializer.""" + # RESPONSE + email_confirmed = serializers.BooleanField() + + # REQUEST + image = serializers.ImageField(required=False) + email = serializers.EmailField(required=False) + username = serializers.CharField(required=False) + class Meta: model = models.User fields = [ - 'first_name', - 'last_name', + 'image', + 'email', + 'email_confirmed', + 'username', ] + def validate_email(self, value): + """Validate email value""" + if value == self.instance.email: + raise serializers.ValidationError() + if not self.instance.email_confirmed: + raise serializers.ValidationError() + return value + + def update(self, instance, validated_data): + """ + Override update method + """ + if 'email' in validated_data: + validated_data['email_confirmed'] = False + instance = super().update(instance, validated_data) + # 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 + + +class ChangePasswordSerializer(serializers.ModelSerializer): + """Serializer for model User.""" + + password = serializers.CharField(write_only=True) + + class Meta: + """Meta class""" + model = models.User + fields = ('password', ) + + def validate(self, attrs): + """Override validate method""" + password = attrs.get('password') + try: + # Compare new password with the old ones + if self.instance.check_password(raw_password=password): + raise utils_exceptions.PasswordsAreEqual() + # Validate password + password_validators.validate_password(password=password) + except serializers.ValidationError as e: + raise serializers.ValidationError(str(e)) + else: + return attrs + + def update(self, instance, validated_data): + """Override update method""" + # Update user password from instance + instance.set_password(validated_data.get('password')) + instance.save() + + # Expire tokens + instance.expire_access_tokens() + instance.expire_refresh_tokens() + return instance + + +class ChangeEmailSerializer(serializers.ModelSerializer): + """Change user email serializer""" + + class Meta: + """Meta class""" + model = models.User + fields = ( + 'email', + ) + + def validate_email(self, value): + """Validate email value""" + if value == self.instance.email: + raise serializers.ValidationError() + return value + + def validate(self, attrs): + """Override validate method""" + email_confirmed = self.instance.email_confirmed + if not email_confirmed: + 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 + + +class ConfirmEmailSerializer(serializers.ModelSerializer): + """Confirm user email serializer""" + + class Meta: + """Meta class""" + model = models.User + fields = ( + 'email', + ) + + def validate(self, attrs): + """Override validate method""" + email_confirmed = self.instance.email_confirmed + if email_confirmed: + raise serializers.ValidationError() + return attrs + + def update(self, instance, validated_data): + """ + Override update method + """ + # 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 + # Firebase Cloud Messaging serializers class FCMDeviceSerializer(serializers.ModelSerializer): diff --git a/apps/account/serializers/web.py b/apps/account/serializers/web.py index 9d6c4c8c..cee66bfa 100644 --- a/apps/account/serializers/web.py +++ b/apps/account/serializers/web.py @@ -6,10 +6,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from account import models, tasks -from authorization.models import JWTRefreshToken from utils import exceptions as utils_exceptions -from utils.serializers import SourceSerializerMixin -from utils.tokens import GMRefreshToken class PasswordResetSerializer(serializers.ModelSerializer): @@ -94,144 +91,3 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer): # Overdue instance instance.overdue() return instance - - -class ChangePasswordSerializer(serializers.ModelSerializer): - """Serializer for model User.""" - - password = serializers.CharField(write_only=True) - - class Meta: - """Meta class""" - model = models.User - fields = ('password', ) - - def validate(self, attrs): - """Override validate method""" - password = attrs.get('password') - try: - # Compare new password with the old ones - if self.instance.check_password(raw_password=password): - raise utils_exceptions.PasswordsAreEqual() - # Validate password - password_validators.validate_password(password=password) - except serializers.ValidationError as e: - raise serializers.ValidationError(str(e)) - else: - return attrs - - def update(self, instance, validated_data): - """Override update method""" - # Update user password from instance - instance.set_password(validated_data.get('password')) - instance.save() - - # Expire tokens - instance.expire_access_tokens() - instance.expire_refresh_tokens() - return instance - - -class ChangeEmailSerializer(serializers.ModelSerializer): - """Change user email serializer""" - - class Meta: - """Meta class""" - model = models.User - fields = ( - 'email', - ) - - def validate_email(self, value): - """Validate email value""" - if value == self.instance.email: - raise serializers.ValidationError() - return value - - def validate(self, attrs): - """Override validate method""" - email_confirmed = self.instance.email_confirmed - if not email_confirmed: - 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 - - -class ConfirmEmailSerializer(serializers.ModelSerializer): - """Confirm user email serializer""" - - class Meta: - """Meta class""" - model = models.User - fields = ( - 'email', - ) - - def validate(self, attrs): - """Override validate method""" - email_confirmed = self.instance.email_confirmed - if email_confirmed: - raise serializers.ValidationError() - return attrs - - def update(self, instance, validated_data): - """ - Override update method - """ - # 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 - - -class RefreshTokenSerializer(SourceSerializerMixin): - """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""" - - cookie_refresh_token = self.get_request().COOKIES.get('refresh_token') - # Check if refresh_token in COOKIES - if not cookie_refresh_token: - raise utils_exceptions.NotValidRefreshTokenError() - - refresh_token = GMRefreshToken(cookie_refresh_token) - refresh_token_qs = JWTRefreshToken.objects.valid() \ - .by_jti(jti=refresh_token.payload.get('jti')) - # Check if the user has refresh token - if not refresh_token_qs.exists(): - raise utils_exceptions.NotValidRefreshTokenError() - - old_refresh_token = refresh_token_qs.first() - source = old_refresh_token.source - user = old_refresh_token.user - - # Expire existing tokens - old_refresh_token.expire() - old_refresh_token.access_token.expire() - - # Create new one for user - response = user.create_jwt_tokens(source=source) - - return response diff --git a/apps/account/urls/common.py b/apps/account/urls/common.py index 6a909588..ec380c2c 100644 --- a/apps/account/urls/common.py +++ b/apps/account/urls/common.py @@ -6,5 +6,12 @@ from account.views import common as views app_name = 'account' urlpatterns = [ - path('user/', views.UserView.as_view(), name='user-get-update'), + path('user/', views.UserRetrieveUpdateView.as_view(), + name='user-retrieve-update'), + path('change-password/', views.ChangePasswordView.as_view(), name='change-password'), + path('change-email/confirm///', views.ChangeEmailConfirmView.as_view(), + name='change-email-confirm'), + path('confirm-email/', views.ConfirmEmailView.as_view(), name='confirm-email'), + path('confirm-email///', views.ConfirmInactiveEmailView.as_view(), + name='inactive-email-confirm'), ] diff --git a/apps/account/urls/web.py b/apps/account/urls/web.py index 5c483d39..cc57f316 100644 --- a/apps/account/urls/web.py +++ b/apps/account/urls/web.py @@ -7,19 +7,11 @@ 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('form/reset-password///', views.FormPasswordResetConfirmView.as_view(), name='form-password-reset-confirm'), 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'), - path('confirm-email/', views.ConfirmEmailView.as_view(), name='confirm-email'), - path('confirm-email///', views.ConfirmInactiveEmailView.as_view(), - name='inactive-email-confirm'), ] urlpatterns = urlpatterns_api + \ diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 5e5f8734..cd17b87d 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -1,17 +1,23 @@ """Common account views""" +from django.utils.encoding import force_text +from django.utils.http import urlsafe_base64_decode from fcm_django.models import FCMDevice -from rest_framework import generics, status +from rest_framework import generics from rest_framework import permissions -from utils.permissions import IsAuthenticatedAndTokenIsValid +from rest_framework import status from rest_framework.response import Response from account import models from account.serializers import common as serializers +from utils import exceptions as utils_exceptions +from utils.models import GMTokenGenerator +from utils.views import (JWTUpdateAPIView, + JWTGenericViewMixin) # User views -class UserView(generics.RetrieveUpdateAPIView): - """### User update view.""" +class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView): + """User update view.""" serializer_class = serializers.UserSerializer queryset = models.User.objects.active() @@ -19,6 +25,86 @@ class UserView(generics.RetrieveUpdateAPIView): return self.request.user +class ChangePasswordView(JWTUpdateAPIView): + """Change password view""" + serializer_class = serializers.ChangePasswordSerializer + queryset = models.User.objects.active() + + def patch(self, request, *args, **kwargs): + """Implement PUT method""" + serializer = self.get_serializer(instance=self.request.user, + data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(status=status.HTTP_200_OK) + + +class ConfirmEmailView(JWTGenericViewMixin): + """Confirm email view.""" + serializer_class = serializers.ConfirmEmailSerializer + 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() + # Expire user tokens + user.expire_access_tokens() + user.expire_refresh_tokens() + + return Response(status=status.HTTP_200_OK) + else: + raise utils_exceptions.UserNotFoundError() + + +class ConfirmInactiveEmailView(generics.GenericAPIView): + """View for confirm inactive 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() + + # Firebase Cloud Messaging class FCMDeviceViewSet(generics.GenericAPIView): """FCMDevice registration view. diff --git a/apps/account/views/web.py b/apps/account/views/web.py index 39dd11fc..af147ba6 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -12,11 +12,9 @@ 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 generics from rest_framework import permissions from rest_framework import status from rest_framework import views -from rest_framework.permissions import AllowAny from rest_framework.response import Response from account import models @@ -25,7 +23,6 @@ from account.serializers import web as serializers from utils import exceptions as utils_exceptions from utils.models import GMTokenGenerator from utils.views import (JWTCreateAPIView, - JWTUpdateAPIView, JWTGenericViewMixin) @@ -76,108 +73,6 @@ class PasswordResetConfirmView(JWTGenericViewMixin): return Response(status=status.HTTP_200_OK) -class ChangePasswordView(JWTUpdateAPIView): - """Change password view""" - serializer_class = serializers.ChangePasswordSerializer - queryset = models.User.objects.active() - - def patch(self, request, *args, **kwargs): - """Implement PUT method""" - serializer = self.get_serializer(instance=self.request.user, - data=request.data) - serializer.is_valid(raise_exception=True) - serializer.save() - 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 ConfirmEmailView(ChangeEmailView): - """Confirm email view.""" - serializer_class = serializers.ConfirmEmailSerializer - - -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() - # Expire user tokens - user.expire_access_tokens() - user.expire_refresh_tokens() - - return Response(status=status.HTTP_200_OK) - else: - raise utils_exceptions.UserNotFoundError() - - -class ConfirmInactiveEmailView(generics.GenericAPIView): - """View for confirm inactive 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 = (AllowAny, ) - 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 5db0e5ed..551b31c9 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -8,9 +8,11 @@ from rest_framework import validators as rest_validators from account import models as account_models from authorization import tasks +from authorization.models import JWTRefreshToken from utils import exceptions as utils_exceptions from utils import methods as utils_methods from utils.serializers import SourceSerializerMixin +from utils.tokens import GMRefreshToken # Serializers @@ -123,6 +125,44 @@ class LogoutSerializer(SourceSerializerMixin): """Serializer for Logout endpoint.""" +class RefreshTokenSerializer(SourceSerializerMixin): + """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""" + + cookie_refresh_token = self.get_request().COOKIES.get('refresh_token') + # Check if refresh_token in COOKIES + if not cookie_refresh_token: + raise utils_exceptions.NotValidRefreshTokenError() + + refresh_token = GMRefreshToken(cookie_refresh_token) + refresh_token_qs = JWTRefreshToken.objects.valid() \ + .by_jti(jti=refresh_token.payload.get('jti')) + # Check if the user has refresh token + if not refresh_token_qs.exists(): + raise utils_exceptions.NotValidRefreshTokenError() + + old_refresh_token = refresh_token_qs.first() + source = old_refresh_token.source + user = old_refresh_token.user + + # Expire existing tokens + old_refresh_token.expire() + old_refresh_token.access_token.expire() + + # Create new one for user + response = user.create_jwt_tokens(source=source) + + return response + + # OAuth class OAuth2Serialzier(SourceSerializerMixin): """Serializer OAuth2 authorization""" diff --git a/apps/authorization/urls/common.py b/apps/authorization/urls/common.py index dd0fb54f..616f9d99 100644 --- a/apps/authorization/urls/common.py +++ b/apps/authorization/urls/common.py @@ -32,7 +32,8 @@ urlpatterns_jwt = [ path('signup/confirm///', views.VerifyEmailConfirmView.as_view(), name='signup-confirm'), path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'), - path('logout/', views.LogoutView.as_view(), name="logout") + path('logout/', views.LogoutView.as_view(), name="logout"), + path('refresh-token/', views.RefreshTokenView.as_view(), name='refresh-token'), ] diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index 97c1e4b8..bd6c8a26 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -226,3 +226,23 @@ class LogoutView(JWTGenericViewMixin): access_token_obj.refresh_token.expire() return Response(status=status.HTTP_204_NO_CONTENT) + + +# Refresh token +class RefreshTokenView(JWTGenericViewMixin): + """Refresh access_token""" + permission_classes = (permissions.AllowAny, ) + 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) + +