diff --git a/apps/account/views/web.py b/apps/account/views/web.py index bebdd9d6..75ab66c4 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -58,7 +58,7 @@ class VerifyEmailConfirmView(JWTGenericViewMixin): if user_qs.exists(): user = user_qs.first() if not gm_token_generator.check_token(user, token): - raise utils_exceptions.NotValidTokenError() + raise utils_exceptions.NotValidAccessTokenError() # Change email status user.confirm_email() return Response(status=status.HTTP_200_OK) @@ -96,7 +96,7 @@ class PasswordResetConfirmView(JWTGenericViewMixin): obj = get_object_or_404(queryset, **filter_kwargs) if not gm_token_generator.check_token(user=obj.user, token=token): - raise utils_exceptions.NotValidTokenError() + raise utils_exceptions.NotValidAccessTokenError() # May raise a permission denied self.check_object_permissions(self.request, obj) diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index 2a38ca09..baeac5d0 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -154,12 +154,20 @@ class LoginByUsernameOrEmailSerializer(JWTBaseSerializerMixin, serializers.Model class RefreshTokenSerializer(serializers.Serializer): """Serializer for refresh token view""" - refresh_token = serializers.CharField() + 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""" - token = tokens.RefreshToken(attrs['refresh_token']) + 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)} @@ -200,8 +208,11 @@ class LogoutSerializer(serializers.ModelSerializer): def validate(self, attrs): """Override validated data""" request = self.context.get('request') - token = request.headers.get('Authorization') \ - .split(' ')[::-1][0] + # Get token bytes from cookies (result: b'Bearer ') + token_bytes = utils_methods.get_token_from_cookies(request) + # Get token value from bytes + token = token_bytes.decode().split(' ')[::-1][0] + # Get access token obj access_token = tokens.AccessToken(token) # Prepare validated data attrs['user'] = request.user diff --git a/apps/collection/serializers/common.py b/apps/collection/serializers/common.py index 073e554a..69319f74 100644 --- a/apps/collection/serializers/common.py +++ b/apps/collection/serializers/common.py @@ -8,7 +8,7 @@ from location import models as location_models class CollectionSerializer(serializers.ModelSerializer): """Collection serializer""" # RESPONSE - image_url = serializers.SerializerMethodField() + image_url = serializers.ImageField(source='image.image') # COMMON block_size = serializers.JSONField() @@ -46,10 +46,6 @@ class CollectionSerializer(serializers.ModelSerializer): 'block_size', ] - def get_image_url(self, obj): - """Return absolute image URL""" - return obj.image.get_full_image_url(request=self.context.get('request')) - class CollectionItemSerializer(serializers.ModelSerializer): """CollectionItem serializer""" diff --git a/apps/news/filters.py b/apps/news/filters.py index 75e66c36..7af3452d 100644 --- a/apps/news/filters.py +++ b/apps/news/filters.py @@ -8,10 +8,21 @@ class NewsListFilterSet(django_filters.FilterSet): """FilterSet for News list""" is_highlighted = django_filters.BooleanFilter() + title = django_filters.CharFilter(method='by_title') class Meta: """Meta class""" model = models.News fields = ( + 'title', 'is_highlighted', ) + + def by_title(self, queryset, name, value): + """Crappy search by title according to locale""" + if value: + locale = self.request.locale + filters = {f'{name}__{locale}': value} + return queryset.filter(**filters) + else: + return queryset diff --git a/apps/news/serializers/common.py b/apps/news/serializers/common.py index bb041071..904c6f89 100644 --- a/apps/news/serializers/common.py +++ b/apps/news/serializers/common.py @@ -23,7 +23,7 @@ class NewsSerializer(serializers.ModelSerializer): title_translated = serializers.CharField(read_only=True, allow_null=True) subtitle_translated = serializers.CharField(read_only=True, allow_null=True) description_translated = serializers.CharField(read_only=True, allow_null=True) - image_url = serializers.SerializerMethodField() + image_url = serializers.ImageField(source='image.image') class Meta: model = models.News @@ -42,12 +42,6 @@ class NewsSerializer(serializers.ModelSerializer): 'description_translated', ] - def get_image_url(self, obj): - """Return absolute image URL""" - if obj.image: - return obj.image.get_full_image_url(request=self.context.get('request')) - return None - class NewsCreateUpdateSerializer(NewsSerializer): """News update serializer.""" diff --git a/apps/utils/authentication.py b/apps/utils/authentication.py new file mode 100644 index 00000000..044d6d75 --- /dev/null +++ b/apps/utils/authentication.py @@ -0,0 +1,36 @@ +"""Custom authentication based on JWTAuthentication class""" +from rest_framework import HTTP_HEADER_ENCODING +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.settings import api_settings + +from utils.methods import get_token_from_cookies + +AUTH_HEADER_TYPES = api_settings.AUTH_HEADER_TYPES + +if not isinstance(api_settings.AUTH_HEADER_TYPES, (list, tuple)): + AUTH_HEADER_TYPES = (AUTH_HEADER_TYPES,) + +AUTH_HEADER_TYPE_BYTES = set( + h.encode(HTTP_HEADER_ENCODING) + for h in AUTH_HEADER_TYPES +) + + +class GMJWTAuthentication(JWTAuthentication): + """ + An authentication plugin that authenticates requests through a JSON web + token provided in a request cookies. + """ + + def authenticate(self, request): + token = get_token_from_cookies(request) + if token is None: + return None + + raw_token = self.get_raw_token(token) + if raw_token is None: + return None + + validated_token = self.get_validated_token(raw_token) + + return self.get_user(validated_token), None diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index 16eb12fc..5d2c973d 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -70,11 +70,18 @@ class NotValidUsernameError(exceptions.APIException): default_detail = _('Wrong username') -class NotValidTokenError(exceptions.APIException): - """The exception should be thrown when token in url is not valid +class NotValidAccessTokenError(exceptions.APIException): + """The exception should be thrown when access token in url is not valid + """ + status_code = status.HTTP_401_UNAUTHORIZED + default_detail = _('Not valid access token') + + +class NotValidRefreshTokenError(exceptions.APIException): + """The exception should be thrown when refresh token is not valid """ status_code = status.HTTP_400_BAD_REQUEST - default_detail = _('Not valid token') + default_detail = _('Not valid refresh token') class PasswordsAreEqual(exceptions.APIException): diff --git a/apps/utils/methods.py b/apps/utils/methods.py index a26c306c..9dcafa97 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -16,13 +16,23 @@ def generate_code(digits=6, string_output=True): return str(value) if string_output else value +def get_token_from_cookies(request): + """Get access token from request cookies""" + cookies = request.COOKIES + if cookies.get('access_token'): + token = f'Bearer {cookies.get("access_token")}' + return token.encode() + + def get_token_from_request(request): """Get access token from request""" + token = None if 'Authorization' in request.headers: if isinstance(request, HttpRequest): - return request.headers.get('Authorization').split(' ')[::-1][0] - elif isinstance(request, Request): - return request._request.headers.get('Authorization').split(' ')[::-1][0] + token = request.headers.get('Authorization').split(' ')[::-1][0] + if isinstance(request, Request): + token = request.headers.get('Authorization').split(' ')[::-1][0] + return token def username_validator(username: str) -> bool: diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 3e4a1d33..d8510e17 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -1,7 +1,10 @@ """Project custom permissions""" from rest_framework.permissions import BasePermission +from rest_framework_simplejwt.exceptions import TokenBackendError + from authorization.models import BlacklistedAccessToken -from utils.methods import get_token_from_request +from utils.exceptions import NotValidAccessTokenError +from utils.methods import get_token_from_cookies class IsAuthenticatedAndTokenIsValid(BasePermission): @@ -12,12 +15,17 @@ class IsAuthenticatedAndTokenIsValid(BasePermission): def has_permission(self, request, view): """Check permissions by access token and default REST permission IsAuthenticated""" user = request.user - if user and user.is_authenticated: - token = get_token_from_request(request) - # Check if user access token not expired - expired = BlacklistedAccessToken.objects.by_token(token)\ - .by_user(user)\ - .exists() - return not expired + try: + if user and user.is_authenticated: + token_bytes = get_token_from_cookies(request) + # Get access token key + token = token_bytes.decode().split(' ')[1] + # Check if user access token not expired + blacklisted = BlacklistedAccessToken.objects.by_token(token) \ + .by_user(user) \ + .exists() + return not blacklisted + except TokenBackendError: + raise NotValidAccessTokenError() else: return False diff --git a/apps/utils/views.py b/apps/utils/views.py index f413e3cc..b222a4d7 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -67,8 +67,8 @@ class JWTGenericViewMixin(generics.GenericAPIView): """Update COOKIES in response from namedtuple""" for cookie in cookies: # todo: remove config for develop - import os - configuration = os.environ.get('SETTINGS_CONFIGURATION', None) + from os import environ + configuration = environ.get('SETTINGS_CONFIGURATION', None) if configuration == 'development': response.set_cookie(key=cookie.key, value=cookie.value, diff --git a/project/settings/base.py b/project/settings/base.py index 0f040f94..5cd8b5b0 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -207,10 +207,9 @@ REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'utils.pagination.ProjectMobilePagination', 'COERCE_DECIMAL_TO_STRING': False, 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', # JWT - 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'utils.authentication.GMJWTAuthentication', + 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'DEFAULT_VERSION': (AVAILABLE_VERSIONS['current'],), diff --git a/project/settings/development.py b/project/settings/development.py index 8f67c38a..798eb6b0 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -1,7 +1,7 @@ """Development settings.""" from .base import * -ALLOWED_HOSTS = ['gm.id-east.ru', ] +ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126'] SEND_SMS = False SMS_CODE_SHOW = True diff --git a/project/settings/local.py b/project/settings/local.py index 2858973c..93e6948b 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -21,6 +21,3 @@ API_HOST_URL = 'http://%s' % API_HOST BROKER_URL = 'amqp://rabbitmq:5672' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL - -# Increase access token lifetime for local deploy -SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'] = timedelta(days=365)