From 663c003119eaf7c0a4a8de0e5435c3cad1812839 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 15 Aug 2019 14:05:17 +0300 Subject: [PATCH] version 0.0.12: updated JWT views --- apps/authorization/views/common.py | 35 ++++++-- apps/news/views/common.py | 4 +- apps/translation/views.py | 4 +- apps/utils/views.py | 127 ++++++++++++++++++++++++----- project/settings/local.py | 3 + 5 files changed, 143 insertions(+), 30 deletions(-) diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index 4043cefb..02d0e54b 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -19,12 +19,16 @@ from account.models import User from authorization.models import Application from authorization.serializers import common as serializers from utils import exceptions as utils_exceptions -from utils.views import JWTViewMixin +from utils.views import (JWTGenericViewMixin, + JWTCreateAPIView, + JWTDestroyAPIView, + JWTUpdateAPIView, + JWTRetrieveAPIView) # Mixins # JWTAuthView mixin -class JWTAuthViewMixin(JWTViewMixin): +class JWTAuthViewMixin(JWTCreateAPIView): """Mixin for authentication views""" def post(self, request, *args, **kwargs): @@ -98,7 +102,7 @@ class OAuth2ViewMixin(CsrfExemptMixin, OAuthLibMixin, BaseOAuth2ViewMixin): # Sign in via Facebook -class OAuth2SignUpView(OAuth2ViewMixin, JWTAuthViewMixin): +class OAuth2SignUpView(OAuth2ViewMixin, JWTCreateAPIView): """ Implements an endpoint to convert a provider token to an access token @@ -176,7 +180,7 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTAuthViewMixin): # JWT # Sign in via username and password -class SignUpView(JWTAuthViewMixin): +class SignUpView(JWTCreateAPIView): """View for classic signup""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.SignupSerializer @@ -207,18 +211,37 @@ class SignUpView(JWTAuthViewMixin): # Login by username|email + password -class LoginByUsernameOrEmailView(JWTAuthViewMixin): +class LoginByUsernameOrEmailView(JWTCreateAPIView): """Login by email and password""" permission_classes = (permissions.AllowAny,) serializer_class = serializers.LoginByUsernameOrEmailSerializer # Refresh access_token -class RefreshTokenView(JWTAuthViewMixin): +class RefreshTokenView(JWTGenericViewMixin): """Refresh access_token""" permission_classes = (permissions.IsAuthenticated,) serializer_class = serializers.RefreshTokenSerializer + def post(self, request, *args, **kwargs): + _locale = request.COOKIES.get('locale') + try: + locale = self._check_locale(locale=_locale) + + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + response = Response(serializer.data, status=status.HTTP_201_CREATED) + access_token, refresh_token = self._get_tokens_from_cookies(request) + except utils_exceptions.LocaleNotExisted: + raise utils_exceptions.LocaleNotExisted(locale=_locale) + else: + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies(locale=locale, + access_token=access_token, + refresh_token=refresh_token), + response=response) + # Logout class LogoutView(generics.CreateAPIView): diff --git a/apps/news/views/common.py b/apps/news/views/common.py index e7d149e0..f3c2d7d1 100644 --- a/apps/news/views/common.py +++ b/apps/news/views/common.py @@ -1,10 +1,10 @@ from rest_framework import generics, permissions from news.models import News from news.serializers import common as serializers -from utils.views import JWTViewMixin +from utils.views import JWTGenericViewMixin -class NewsList(JWTViewMixin, generics.ListAPIView): +class NewsList(generics.ListAPIView): """News list view.""" queryset = News.objects.all() permission_classes = (permissions.AllowAny, ) diff --git a/apps/translation/views.py b/apps/translation/views.py index cce82e1c..a20c1c67 100644 --- a/apps/translation/views.py +++ b/apps/translation/views.py @@ -2,7 +2,7 @@ from rest_framework import generics from translation import models from translation import serializers from rest_framework import permissions -from utils.views import JWTViewMixin +from utils.views import JWTGenericViewMixin # Mixins @@ -13,7 +13,7 @@ class LanguageViewMixin(generics.GenericAPIView): # Views -class LanguageListView(LanguageViewMixin, JWTViewMixin, generics.ListAPIView): +class LanguageListView(LanguageViewMixin, generics.ListAPIView): """List view for model Language""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.LanguageSerializer diff --git a/apps/utils/views.py b/apps/utils/views.py index d2602c79..721dbd08 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -3,19 +3,20 @@ from collections import namedtuple from translation import models as translation_models from utils import exceptions from rest_framework.response import Response +from rest_framework import status # JWT # Login base view mixin -class JWTViewMixin(generics.GenericAPIView): +class JWTGenericViewMixin(generics.GenericAPIView): """JWT view mixin""" - ACCESS_TOKEN_HTTP = True + ACCESS_TOKEN_HTTP_ONLY = True ACCESS_TOKEN_SECURE = False - REFRESH_TOKEN_HTTP = True + REFRESH_TOKEN_HTTP_ONLY = True REFRESH_TOKEN_SECURE = False - COOKIE = namedtuple('COOKIE', ['key', 'value', 'http', 'secure']) + COOKIE = namedtuple('COOKIE', ['key', 'value', 'http_only', 'secure']) def _check_locale(self, locale: str): @@ -24,10 +25,7 @@ class JWTViewMixin(generics.GenericAPIView): raise exceptions.LocaleNotExisted() return locale - def _put_data_in_cookies(self, - locale: str, - access_token: str, - refresh_token: str): + def _put_data_in_cookies(self, locale: str, access_token: str, refresh_token: str): """ CHECK locale in cookies and PUT access and refresh tokens there. cookies it is list that contain namedtuples @@ -36,22 +34,21 @@ class JWTViewMixin(generics.GenericAPIView): COOKIES = list() # Create locale namedtuple - locale = self.COOKIE(key='locale', - value=locale, - http=True, - secure=False) - COOKIES.append(locale) + _locale = self.COOKIE(key='locale', + value=locale, + http_only=True, + secure=False) # Write to cookie access and refresh token with secure flag _access_token = self.COOKIE(key='access_token', value=access_token, - http=self.ACCESS_TOKEN_HTTP, + http_only=self.ACCESS_TOKEN_HTTP_ONLY, secure=self.ACCESS_TOKEN_SECURE) _refresh_token = self.COOKIE(key='refresh_token', value=refresh_token, - http=self.REFRESH_TOKEN_HTTP, + http_only=self.REFRESH_TOKEN_HTTP_ONLY, secure=self.REFRESH_TOKEN_SECURE) - COOKIES.extend((_access_token, _refresh_token)) + COOKIES.extend((_locale, _access_token, _refresh_token)) return COOKIES def _put_cookies_in_response(self, cookies: list, response: Response): @@ -59,7 +56,8 @@ class JWTViewMixin(generics.GenericAPIView): for cookie in cookies: response.set_cookie(key=cookie.key, value=cookie.value, - secure=cookie.secure) + secure=cookie.secure, + httponly=cookie.http_only) return response def _get_tokens_from_cookies(self, request, cookies: dict = None): @@ -67,13 +65,43 @@ class JWTViewMixin(generics.GenericAPIView): _cookies = request.COOKIES or cookies return [self.COOKIE(key='access_token', value=_cookies.get('access_token'), - http=self.ACCESS_TOKEN_HTTP, + http_only=self.ACCESS_TOKEN_HTTP_ONLY, secure=self.ACCESS_TOKEN_SECURE), self.COOKIE(key='refresh_token', value=_cookies.get('refresh_token'), - http=self.REFRESH_TOKEN_HTTP, + http_only=self.REFRESH_TOKEN_HTTP_ONLY, secure=self.REFRESH_TOKEN_SECURE)] + +class JWTCreateAPIView(JWTGenericViewMixin, generics.CreateAPIView): + """ + Concrete view for creating a model instance. + """ + def post(self, request, *args, **kwargs): + _locale = request.COOKIES.get('locale') + try: + locale = self._check_locale(locale=_locale) + + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + + response = Response(serializer.data, status=status.HTTP_201_CREATED) + access_token, refresh_token = self._get_tokens_from_cookies(request) + except exceptions.LocaleNotExisted: + raise exceptions.LocaleNotExisted(locale=_locale) + else: + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies(locale=locale, + access_token=access_token, + refresh_token=refresh_token), + response=response) + + +class JWTRetrieveAPIView(JWTGenericViewMixin, generics.RetrieveAPIView): + """ + Concrete view for retrieving a model instance. + """ def get(self, request, *args, **kwargs): """Implement GET method""" _locale = request.COOKIES.get('locale') @@ -87,7 +115,7 @@ class JWTViewMixin(generics.GenericAPIView): response = self.get_paginated_response(serializer.data) else: serializer = self.get_serializer(queryset, many=True) - response = Response(serializer.data) + response = Response(serializer.data, status.HTTP_200_OK) access_token, refresh_token = self._get_tokens_from_cookies(request) @@ -99,3 +127,62 @@ class JWTViewMixin(generics.GenericAPIView): access_token=access_token, refresh_token=refresh_token), response=response) + + +class JWTDestroyAPIView(JWTGenericViewMixin, generics.DestroyAPIView): + """ + Concrete view for deleting a model instance. + """ + def delete(self, request, *args, **kwargs): + _locale = request.COOKIES.get('locale') + try: + locale = self._check_locale(locale=_locale) + instance = self.get_object() + instance.delete() + response = Response(status=status.HTTP_204_NO_CONTENT) + + access_token, refresh_token = self._get_tokens_from_cookies(request) + except exceptions.LocaleNotExisted: + raise exceptions.LocaleNotExisted(locale=_locale) + else: + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies(locale=locale, + access_token=access_token, + refresh_token=refresh_token), + response=response) + + +class JWTUpdateAPIView(JWTGenericViewMixin, generics.UpdateAPIView): + """ + Concrete view for updating a model instance. + """ + def put(self, request, *args, **kwargs): + _locale = request.COOKIES.get('locale') + try: + locale = self._check_locale(locale=_locale) + partial = kwargs.pop('partial', False) + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + serializer.save() + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + + response = Response(serializer.data) + access_token, refresh_token = self._get_tokens_from_cookies(request) + except exceptions.LocaleNotExisted: + raise exceptions.LocaleNotExisted(locale=_locale) + else: + return self._put_cookies_in_response( + cookies=self._put_data_in_cookies(locale=locale, + access_token=access_token, + refresh_token=refresh_token), + response=response) + + def patch(self, request, *args, **kwargs): + kwargs['partial'] = True + return self.put(request, *args, **kwargs) + diff --git a/project/settings/local.py b/project/settings/local.py index 23cd5d9c..f301062a 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -7,3 +7,6 @@ SEND_SMS = False SMS_CODE_SHOW = True DOMAIN_URI = 'localhost:8000' + +# Increase access token lifetime for local deploy +SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'] = timedelta(days=365)