diff --git a/answerking/settings/base.py b/answerking/settings/base.py index 34ed281d..2fc39c44 100644 --- a/answerking/settings/base.py +++ b/answerking/settings/base.py @@ -2,6 +2,7 @@ Django base settings for answerking project.. """ import os +from datetime import timedelta from os.path import join from pathlib import Path @@ -39,6 +40,7 @@ "rest_framework", "corsheaders", "drf_problems", + "rest_framework_simplejwt.token_blacklist", ] MIDDLEWARE = [ @@ -81,6 +83,9 @@ "EXCEPTION_HANDLER": "answerking_app.utils.exceptions_handler.wrapper", "COERCE_DECIMAL_TO_STRING": False, "DATETIME_FORMAT": "%Y-%m-%dT%H:%M:%S.%fZ", + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), } # Database @@ -97,7 +102,7 @@ } } -# Password validation +# Password permissions # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ @@ -120,6 +125,13 @@ # Internationalization # https://docs.djangoproject.com/en/4.0/topics/i18n/ +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, + "UPDATE_LAST_LOGIN": True, +} LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" diff --git a/answerking/urls.py b/answerking/urls.py index ad265f86..83e1ff96 100644 --- a/answerking/urls.py +++ b/answerking/urls.py @@ -7,6 +7,7 @@ path("api/", include("answerking_app.urls.category_urls")), path("api/", include("answerking_app.urls.order_urls")), path("api/", include("answerking_app.urls.tag_urls")), + path("api/", include("answerking_app.urls.auth_urls")), path("admin/", admin.site.urls), path("", include("drf_problems.urls")), ] @@ -19,9 +20,9 @@ ) urlpatterns += [ - path("api/schema/", SpectacularAPIView.as_view(), name="schema"), + path("api/schema", SpectacularAPIView.as_view(), name="schema"), path( - "api/schema/swagger-ui/", + "api/schema/swagger-ui", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui", ), diff --git a/answerking_app/models/models.py b/answerking_app/models/models.py index ff46d5dd..2931cfe8 100644 --- a/answerking_app/models/models.py +++ b/answerking_app/models/models.py @@ -1,5 +1,6 @@ from decimal import Decimal +from django.contrib.auth.models import User from django.db import models @@ -45,6 +46,9 @@ class Status(models.TextChoices): ) created_on = models.DateTimeField(auto_now_add=True) last_updated = models.DateTimeField(auto_now=True) + owner = models.ForeignKey( + "auth.User", related_name="orders", on_delete=models.CASCADE + ) def calculate_total(self): total = Decimal(0.00) diff --git a/answerking_app/models/permissions/auth_permissions.py b/answerking_app/models/permissions/auth_permissions.py new file mode 100644 index 00000000..6461a04b --- /dev/null +++ b/answerking_app/models/permissions/auth_permissions.py @@ -0,0 +1,30 @@ +from rest_framework.permissions import IsAdminUser, BasePermission + + +class IsStaffUser(IsAdminUser): + """ + Allows access to Staff and Managers only. + """ + + pass + + +class IsManagerUser(BasePermission): + """ + Allows access to Manager's only. + """ + + def has_permission(self, request, view): + return bool(request.user and request.user.is_staff) + + +class IsOwner(BasePermission): + """ + Custom permission to only allow owners of an object to edit it. + """ + + def has_permission(self, request, view): + return bool(request.user and request.user.is_authenticated) + + def has_object_permission(self, request, view, obj): + return bool(obj.owner == request.user) diff --git a/answerking_app/models/serializers.py b/answerking_app/models/serializers.py index 4d1d596b..39c5a69e 100644 --- a/answerking_app/models/serializers.py +++ b/answerking_app/models/serializers.py @@ -1,11 +1,17 @@ +import re +from abc import ABC from typing import OrderedDict +from django.contrib.auth.models import User +from django.contrib.auth.password_validation import validate_password from django.core.validators import ( MaxValueValidator, MinValueValidator, RegexValidator, ) from rest_framework import serializers, status +from rest_framework.validators import UniqueValidator +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from answerking_app.models.models import ( Category, @@ -246,9 +252,10 @@ class OrderSerializer(serializers.ModelSerializer): lineItems = LineItemSerializer( source="lineitem_set", many=True, required=False ) + owner = serializers.ReadOnlyField(source="owner.username") def create(self, validated_data: dict) -> Order: - order: Order = Order.objects.create() + order: Order = Order.objects.create(owner=validated_data["owner"]) if "lineitem_set" in validated_data: line_items_data = validated_data["lineitem_set"] self.create_order_line_items( @@ -298,17 +305,95 @@ def validate_lineItems(self, line_items_data): class Meta: model = Order - fields = ( + fields = [ "id", "createdOn", "lastUpdated", "orderStatus", "orderTotal", "lineItems", - ) + "owner", + ] depth = 3 +class ManagerAuthSerializer(serializers.ModelSerializer): + username = serializers.CharField( + required=True, + validators=[UniqueValidator(queryset=User.objects.all())], + max_length=50, + min_length=4, + ) + email = serializers.EmailField( + required=True, + validators=[UniqueValidator(queryset=User.objects.all())], + ) + password = serializers.CharField( + write_only=True, required=True, validators=[validate_password] + ) + password2 = serializers.CharField( + write_only=True, + required=True, + ) + + class Meta: + model = User + fields = ( + "username", + "password", + "password2", + "email", + "first_name", + "last_name", + ) + + def validate(self, attrs): + if attrs["password"] != attrs["password2"]: + raise ProblemDetails( + status=status.HTTP_400_BAD_REQUEST, + detail="The passwords supplied do not match", + ) + return attrs + + def validate_first_name(self, value: str) -> str: + return compress_white_spaces(value) + + def validate_last_name(self, value: str) -> str: + return compress_white_spaces(value) + + def create(self, validated_data): + user = User.objects.create( + username=validated_data["username"], + email=validated_data["email"], + first_name=validated_data["first_name"], + last_name=validated_data["last_name"], + is_staff=True, + is_superuser=False, + ) + + user.set_password(validated_data["password"]) + user.save() + return user + + +class LoginSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + token = super().get_token(user) + token["username"] = user.username + token["email"] = user.email + return token + + def validate(self, attrs): + attrs = super().validate(attrs) + return { + "username": self.user.username, + "email": self.user.email, + "first_name": self.user.first_name, + **attrs, + } + + class ErrorDetailSerializer(serializers.Serializer): name = serializers.CharField() diff --git a/answerking_app/urls/auth_urls.py b/answerking_app/urls/auth_urls.py new file mode 100644 index 00000000..51948ed7 --- /dev/null +++ b/answerking_app/urls/auth_urls.py @@ -0,0 +1,17 @@ +from functools import partial + +from django.urls import path +from rest_framework_simplejwt.views import TokenRefreshView, TokenBlacklistView + +from answerking_app.views import auth_views + +urlpatterns: list[partial] = [ + path( + "register/manager", + auth_views.RegisterManagerView.as_view(), + name="register_manager", + ), + path("login", auth_views.LoginView.as_view(), name="token_obtain_pair"), + path("login/refresh", TokenRefreshView.as_view(), name="token_refresh"), + path("logout", TokenBlacklistView.as_view(), name="token_blacklist"), +] diff --git a/answerking_app/urls/category_urls.py b/answerking_app/urls/category_urls.py index 08f24ddf..d71466e4 100644 --- a/answerking_app/urls/category_urls.py +++ b/answerking_app/urls/category_urls.py @@ -7,17 +7,17 @@ urlpatterns: list[partial] = [ path( "categories", - category_views.CategoryListView.as_view(), - name="category_list", + category_views.CategoryView.as_view(), + name="category", ), path( "categories/", - category_views.CategoryDetailView.as_view(), - name="category_detail", + category_views.CategoryIdView.as_view(), + name="category_id", ), path( "categories//products", - category_views.CategoryProductListView.as_view(), - name="category_product_list", + category_views.CategoryProductView.as_view(), + name="category_product", ), ] diff --git a/answerking_app/urls/order_urls.py b/answerking_app/urls/order_urls.py index 5a09dce2..c47c2158 100644 --- a/answerking_app/urls/order_urls.py +++ b/answerking_app/urls/order_urls.py @@ -5,7 +5,7 @@ from answerking_app.views import order_views urlpatterns: list[partial] = [ - path("orders", order_views.OrderListView.as_view(), name="order_list"), + path("orders", order_views.OrderView.as_view(), name="order_list"), path( "orders/", order_views.OrderDetailView.as_view(), diff --git a/answerking_app/urls/product_urls.py b/answerking_app/urls/product_urls.py index 7920b4b8..d40a995e 100644 --- a/answerking_app/urls/product_urls.py +++ b/answerking_app/urls/product_urls.py @@ -7,7 +7,7 @@ urlpatterns: list[partial] = [ path( "products", - product_views.ProductListView.as_view(), + product_views.ProductView.as_view(), name="product_list", ), path( diff --git a/answerking_app/utils/schema/schema_examples.py b/answerking_app/utils/schema/schema_examples.py index 14da99d8..54a8d3ad 100644 --- a/answerking_app/utils/schema/schema_examples.py +++ b/answerking_app/utils/schema/schema_examples.py @@ -118,7 +118,7 @@ problem_detail_example: ProblemDetails = { "errors": {"name": "The name field is required."}, "type": "https://testserver/problems/error/", - "title": "One or more validation errors occurred.", + "title": "One or more permissions errors occurred.", "status": 400, "traceID": "00-f40e09a437a87f4ebcd2f39b128bb8f3-4b2ad798ac046140-00", "detail": "string", diff --git a/answerking_app/views/auth_views.py b/answerking_app/views/auth_views.py new file mode 100644 index 00000000..d280293f --- /dev/null +++ b/answerking_app/views/auth_views.py @@ -0,0 +1,21 @@ +from rest_framework import generics +from rest_framework.permissions import AllowAny +from rest_framework_simplejwt.views import TokenObtainPairView + +from answerking_app.models.serializers import ( + ManagerAuthSerializer, + LoginSerializer, +) + + +class RegisterManagerView( + generics.CreateAPIView, +): + permission_classes = (AllowAny,) + serializer_class = ManagerAuthSerializer + + +class LoginView( + TokenObtainPairView, +): + serializer_class = LoginSerializer diff --git a/answerking_app/views/category_views.py b/answerking_app/views/category_views.py index 1db956c2..81b6e5b3 100644 --- a/answerking_app/views/category_views.py +++ b/answerking_app/views/category_views.py @@ -21,7 +21,6 @@ ) from answerking_app.utils.schema.schema_examples import ( category_example, - retired_category_example, category_body_example, problem_detail_example, category_products_body_example, @@ -29,11 +28,14 @@ ) -class CategoryListView( +# classes for each endpoint and type of request + + +class CategoryGetView( mixins.ListModelMixin, - mixins.CreateModelMixin, generics.GenericAPIView, ): + permission_classes = [] queryset: QuerySet = Category.objects.all() serializer_class: CategorySerializer = CategorySerializer @@ -57,6 +59,15 @@ class CategoryListView( def get(self, request: Request, *args, **kwargs) -> Response: return self.list(request, *args, **kwargs) + +class CategoryPostView( + mixins.CreateModelMixin, + generics.GenericAPIView, +): + permission_classes = [] + queryset: QuerySet = Category.objects.all() + serializer_class: CategorySerializer = CategorySerializer + @extend_schema( tags=["Inventory"], summary="Create a new category.", @@ -94,13 +105,11 @@ def post(self, request: Request, *args, **kwargs) -> Response: return self.create(request, *args, **kwargs) -class CategoryDetailView( +class CategoryIdGetView( mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - RetireMixin, - mixins.DestroyModelMixin, generics.GenericAPIView, ): + permission_classes = [] queryset: QuerySet = Category.objects.all() serializer_class: CategorySerializer = CategorySerializer @@ -147,6 +156,15 @@ def get(self, request: Request, *args, **kwargs) -> Response: check_url_parameter(kwargs["pk"]) return self.retrieve(request, *args, **kwargs) + +class CategoryIdPutView( + mixins.UpdateModelMixin, + generics.GenericAPIView, +): + permission_classes = [] + queryset: QuerySet = Category.objects.all() + serializer_class: CategorySerializer = CategorySerializer + @extend_schema( tags=["Inventory"], summary="Update an existing category", @@ -197,6 +215,15 @@ def put(self, request: Request, *args, **kwargs) -> Response: check_url_parameter(kwargs["pk"]) return self.update(request, *args, **kwargs) + +class CategoryIdDeleteView( + RetireMixin, + generics.GenericAPIView, +): + permission_classes = [] + queryset: QuerySet = Category.objects.all() + serializer_class: CategorySerializer = CategorySerializer + @extend_schema( tags=["Inventory"], summary="Retire an existing category", @@ -242,10 +269,11 @@ def delete(self, request: Request, *args, **kwargs) -> Response: return self.retire(request, *args, **kwargs) -class CategoryProductListView( +class CategoryProductGetView( CategoryProductListMixin, generics.GenericAPIView, ): + permission_classes = [] queryset: QuerySet = Category.objects.all() serializer_class: CategorySerializer = CategorySerializer @@ -291,3 +319,20 @@ class CategoryProductListView( def get(self, request: Request, **kwargs) -> Response: check_url_parameter(kwargs["pk"]) return self.list(**kwargs) + + +# classes grouping all the requests for one endpoint + + +class CategoryView(CategoryGetView, CategoryPostView): + pass + + +class CategoryIdView( + CategoryIdGetView, CategoryIdPutView, CategoryIdDeleteView +): + pass + + +class CategoryProductView(CategoryProductGetView): + pass diff --git a/answerking_app/views/order_views.py b/answerking_app/views/order_views.py index f5d307ac..b19c3c8b 100644 --- a/answerking_app/views/order_views.py +++ b/answerking_app/views/order_views.py @@ -2,10 +2,15 @@ from django.db.models import QuerySet from rest_framework import generics, mixins +from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response from answerking_app.models.models import Order +from answerking_app.models.permissions.auth_permissions import ( + IsOwner, + IsStaffUser, +) from answerking_app.models.serializers import ( OrderSerializer, ProblemDetailSerializer, @@ -26,13 +31,10 @@ ) -class OrderListView( - mixins.ListModelMixin, - mixins.CreateModelMixin, - generics.GenericAPIView, -): +class OrderListView(mixins.ListModelMixin, generics.GenericAPIView): queryset: QuerySet = Order.objects.all() serializer_class: OrderSerializer = OrderSerializer + permission_classes = [IsStaffUser] @extend_schema( tags=["Orders"], @@ -50,6 +52,12 @@ class OrderListView( def get(self, request: Request, *args, **kwargs) -> Response: return self.list(request, *args, **kwargs) + +class OrderCreateView(mixins.CreateModelMixin, generics.GenericAPIView): + queryset: QuerySet = Order.objects.all() + serializer_class: OrderSerializer = OrderSerializer + permission_classes = [IsAuthenticated] + @extend_schema( tags=["Orders"], summary="Create a new order.", @@ -86,17 +94,18 @@ def get(self, request: Request, *args, **kwargs) -> Response: def post(self, request: Request, *args, **kwargs) -> Response: return self.create(request) + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + -class OrderDetailView( +class OrderRetrieveView( mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - CancelOrderMixin, generics.GenericAPIView, ): - queryset: QuerySet = Order.objects.all() serializer_class: OrderSerializer = OrderSerializer lookup_url_kwarg: Literal["pk"] = "pk" + permission_classes = [IsOwner, IsStaffUser] @extend_schema( tags=["Orders"], @@ -130,6 +139,16 @@ def get(self, request: Request, *args, **kwargs) -> Response: check_url_parameter(kwargs["pk"]) return self.retrieve(request, *args, **kwargs) + +class OrderUpdateView( + mixins.UpdateModelMixin, + generics.GenericAPIView, +): + queryset: QuerySet = Order.objects.all() + serializer_class: OrderSerializer = OrderSerializer + lookup_url_kwarg: Literal["pk"] = "pk" + permission_classes = [IsOwner, IsStaffUser] + @extend_schema( tags=["Orders"], summary="Update an existing order.", @@ -180,6 +199,16 @@ def put(self, request: Request, *args, **kwargs) -> Response: check_url_parameter(kwargs["pk"]) return self.update(request, *args, **kwargs) + +class OrderCancelView( + CancelOrderMixin, + generics.GenericAPIView, +): + queryset: QuerySet = Order.objects.all() + serializer_class: OrderSerializer = OrderSerializer + lookup_url_kwarg: Literal["pk"] = "pk" + permission_classes = [IsOwner, IsStaffUser] + @extend_schema( tags=["Orders"], summary="Cancel an existing order", @@ -212,3 +241,11 @@ def put(self, request: Request, *args, **kwargs) -> Response: def delete(self, request: Request, *args, **kwargs) -> Response: check_url_parameter(kwargs["pk"]) return self.cancel_order(request, *args, **kwargs) + + +class OrderView(OrderListView, OrderCreateView): + pass + + +class OrderDetailView(OrderRetrieveView, OrderUpdateView, OrderCancelView): + pass diff --git a/answerking_app/views/product_views.py b/answerking_app/views/product_views.py index cea612ee..1ad812db 100644 --- a/answerking_app/views/product_views.py +++ b/answerking_app/views/product_views.py @@ -5,6 +5,7 @@ extend_schema, ) from rest_framework import generics, mixins +from rest_framework.permissions import AllowAny from rest_framework.request import Request from rest_framework.response import Response @@ -19,19 +20,18 @@ product_body_example, product_categories_body_example, product_example, - retired_product_example, ) from answerking_app.utils.url_parameter_check import check_url_parameter +from answerking_app.models.permissions.auth_permissions import IsStaffUser class ProductListView( mixins.ListModelMixin, - mixins.CreateModelMixin, generics.GenericAPIView, ): - queryset: QuerySet = Product.objects.all() serializer_class: ProductSerializer = ProductSerializer + permission_classes = [AllowAny] @extend_schema( tags=["Inventory"], @@ -53,6 +53,15 @@ class ProductListView( def get(self, request: Request, *args, **kwargs) -> Response: return self.list(request, *args, **kwargs) + +class ProductCreateView( + mixins.CreateModelMixin, + generics.GenericAPIView, +): + queryset: QuerySet = Product.objects.all() + serializer_class: ProductSerializer = ProductSerializer + permission_classes = [IsStaffUser] + @extend_schema( tags=["Inventory"], summary="Create a new product.", @@ -90,13 +99,10 @@ def post(self, request: Request, *args, **kwargs) -> Response: return self.create(request, *args, **kwargs) -class ProductDetailView( - RetireMixin, - generics.UpdateAPIView, - mixins.RetrieveModelMixin, -): +class ProductRetrieveView(mixins.RetrieveModelMixin): queryset: QuerySet = Product.objects.all() serializer_class: ProductSerializer = ProductSerializer + permission_classes = [AllowAny] @extend_schema( tags=["Inventory"], @@ -141,6 +147,12 @@ def get(self, request: Request, *args, **kwargs) -> Response: check_url_parameter(kwargs["pk"]) return self.retrieve(request, *args, **kwargs) + +class ProductUpdateView(generics.UpdateAPIView): + queryset: QuerySet = Product.objects.all() + serializer_class: ProductSerializer = ProductSerializer + permission_classes = [IsStaffUser] + @extend_schema( tags=["Inventory"], summary="Update an existing product", @@ -191,6 +203,12 @@ def put(self, request: Request, *args, **kwargs) -> Response: check_url_parameter(kwargs["pk"]) return self.update(request, *args, **kwargs) + +class ProductRetireView(RetireMixin): + queryset: QuerySet = Product.objects.all() + serializer_class: ProductSerializer = ProductSerializer + permission_classes = [IsStaffUser] + @extend_schema( tags=["Inventory"], summary="Retire an existing product", @@ -234,3 +252,13 @@ def put(self, request: Request, *args, **kwargs) -> Response: def delete(self, request: Request, *args, **kwargs) -> Response: check_url_parameter(kwargs["pk"]) return self.retire(request, *args, **kwargs) + + +class ProductView(ProductListView, ProductCreateView): + pass + + +class ProductDetailView( + ProductRetrieveView, ProductUpdateView, ProductRetireView +): + pass diff --git a/poetry.lock b/poetry.lock index 3416eca8..7657f908 100644 --- a/poetry.lock +++ b/poetry.lock @@ -187,6 +187,27 @@ python-versions = ">=3.6" django = ">=3.0" pytz = "*" +[[package]] +name = "djangorestframework-simplejwt" +version = "5.2.2" +description = "A minimal JSON Web Token authentication plugin for Django REST Framework" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +django = "*" +djangorestframework = "*" +pyjwt = ">=1.7.1,<3" + +[package.extras] +crypto = ["cryptography (>=3.3.1)"] +dev = ["Sphinx (>=1.6.5,<2)", "cryptography", "flake8", "ipython", "isort", "pep8", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "sphinx-rtd-theme (>=0.1.9)", "tox", "twine", "wheel"] +doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"] +lint = ["flake8", "isort", "pep8"] +python-jose = ["python-jose (==3.3.0)"] +test = ["cryptography", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] + [[package]] name = "drf-ignore-slash-middleware" version = "0.0.1" @@ -437,6 +458,20 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "pyjwt" +version = "2.6.0" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pyright" version = "1.1.294" @@ -661,7 +696,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "913c9062006d9be5e07954128bf6dc09516d215a4c4587e5d22a3db993d74dda" +content-hash = "63c0fcd254fd579aa595e4506f831bfc132a24de6011201927e2537e1b055903" [metadata.files] asgiref = [ @@ -868,6 +903,10 @@ djangorestframework = [ {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, ] +djangorestframework-simplejwt = [ + {file = "djangorestframework_simplejwt-5.2.2-py3-none-any.whl", hash = "sha256:4c0d2e2513e12587d93501ac091781684a216c3ee614eb3b5a10586aef5ca845"}, + {file = "djangorestframework_simplejwt-5.2.2.tar.gz", hash = "sha256:d27d4bcac2c6394f678dea8b4d0d511c6e18a7f2eb8aaeeb8a7de601aeb77c42"}, +] drf-ignore-slash-middleware = [ {file = "drf_ignore_slash_middleware-0.0.1-py3-none-any.whl", hash = "sha256:9c17b6c2ce16685455df4be0fe7d34c5f6fa4c61ddb4660ca5f253ea08914469"}, {file = "drf_ignore_slash_middleware-0.0.1.tar.gz", hash = "sha256:3ed51accc471b44c8e7986e3667669f74e7fa303a516f774d7931d069c1d6f0d"}, @@ -1005,6 +1044,10 @@ pycodestyle = [ {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, ] +pyjwt = [ + {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"}, + {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"}, +] pyright = [ {file = "pyright-1.1.294-py3-none-any.whl", hash = "sha256:5b27e28a1cfc60cea707fd3b644769fa6dd0b194481cdcc2399cf2a51cc5a846"}, {file = "pyright-1.1.294.tar.gz", hash = "sha256:fea5fed3d6a3f02259e622c901e86a7b8bcf237d35e1cdfe01d0e0723768dcb6"}, diff --git a/pyproject.toml b/pyproject.toml index 019a95ef..f182b13a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ drf-spectacular = "^0.25.1" gunicorn = "^20.1.0" generics = "^6.0.0" drf-ignore-slash-middleware = "^0.0.1" +djangorestframework-simplejwt = "^5.2.2" [tool.poetry.group.dev.dependencies] black = "^22.10.0"