Features: 1) Add rate-limiting decorators for multiple API methods across core viewsets; 2) Add translation support to messaging documentation.

Fixes: 1) Fix missing import for `send_message` moved to method scope in Telegram message handler; 2) Correct Swagger UI socket connection setting to `False`.

Extra: 1) Minor code cleanup and reformatting in viewsets and settings.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-11-12 11:50:51 +03:00
parent 554769d48e
commit de0cb836fc
4 changed files with 17 additions and 2 deletions

View file

@ -266,6 +266,7 @@ class CategoryViewSet(EvibesViewSet):
AllowAny, AllowAny,
], ],
) )
@method_decorator(ratelimit(key="ip", rate="4/s" if not settings.DEBUG else "44/s"))
def seo_meta(self, request: Request, *args, **kwargs) -> Response: def seo_meta(self, request: Request, *args, **kwargs) -> Response:
category = self.get_object() category = self.get_object()
@ -506,6 +507,7 @@ class ProductViewSet(EvibesViewSet):
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@action(detail=True, methods=["get"], url_path="feedbacks") @action(detail=True, methods=["get"], url_path="feedbacks")
@method_decorator(ratelimit(key="ip", rate="2/s" if not settings.DEBUG else "44/s"))
def feedbacks(self, request: Request, *args, **kwargs) -> Response: def feedbacks(self, request: Request, *args, **kwargs) -> Response:
product = self.get_object() product = self.get_object()
qs = Feedback.objects.filter(order_product__product=product) qs = Feedback.objects.filter(order_product__product=product)
@ -522,6 +524,7 @@ class ProductViewSet(EvibesViewSet):
AllowAny, AllowAny,
], ],
) )
@method_decorator(ratelimit(key="ip", rate="4/s" if not settings.DEBUG else "44/s"))
def seo_meta(self, request: Request, *args, **kwargs) -> Response: def seo_meta(self, request: Request, *args, **kwargs) -> Response:
p = self.get_object() p = self.get_object()
images = list(p.images.all()[:6]) images = list(p.images.all()[:6])
@ -561,6 +564,10 @@ class ProductViewSet(EvibesViewSet):
} }
return Response(SeoSnapshotSerializer(payload).data) return Response(SeoSnapshotSerializer(payload).data)
@method_decorator(ratelimit(key="ip", rate="4/s" if not settings.DEBUG else "44/s"))
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
@extend_schema_view(**VENDOR_SCHEMA) @extend_schema_view(**VENDOR_SCHEMA)
class VendorViewSet(EvibesViewSet): class VendorViewSet(EvibesViewSet):
@ -665,6 +672,7 @@ class OrderViewSet(EvibesViewSet):
return obj return obj
@action(detail=False, methods=["get"], url_path="current") @action(detail=False, methods=["get"], url_path="current")
@method_decorator(ratelimit(key="ip", rate="1/s" if not settings.DEBUG else "44/s"))
def current(self, request: Request, *args, **kwargs) -> Response: def current(self, request: Request, *args, **kwargs) -> Response:
if not request.user.is_authenticated: if not request.user.is_authenticated:
raise PermissionDenied(permission_denied_message) raise PermissionDenied(permission_denied_message)
@ -678,6 +686,7 @@ class OrderViewSet(EvibesViewSet):
) )
@action(detail=True, methods=["post"], url_path="buy") @action(detail=True, methods=["post"], url_path="buy")
@method_decorator(ratelimit(key="ip", rate="1/s" if not settings.DEBUG else "44/s"))
def buy(self, request: Request, *args, **kwargs) -> Response: def buy(self, request: Request, *args, **kwargs) -> Response:
serializer = BuyOrderSerializer(data=request.data) serializer = BuyOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -731,6 +740,7 @@ class OrderViewSet(EvibesViewSet):
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(e)}) return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(e)})
@action(detail=True, methods=["post"], url_path="add_order_product") @action(detail=True, methods=["post"], url_path="add_order_product")
@method_decorator(ratelimit(key="ip", rate="1/s" if not settings.DEBUG else "44/s"))
def add_order_product(self, request: Request, *args, **kwargs) -> Response: def add_order_product(self, request: Request, *args, **kwargs) -> Response:
serializer = AddOrderProductSerializer(data=request.data) serializer = AddOrderProductSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -750,6 +760,7 @@ class OrderViewSet(EvibesViewSet):
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)}) return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)})
@action(detail=True, methods=["post"], url_path="remove_order_product") @action(detail=True, methods=["post"], url_path="remove_order_product")
@method_decorator(ratelimit(key="ip", rate="1/s" if not settings.DEBUG else "44/s"))
def remove_order_product(self, request: Request, *args, **kwargs) -> Response: def remove_order_product(self, request: Request, *args, **kwargs) -> Response:
serializer = RemoveOrderProductSerializer(data=request.data) serializer = RemoveOrderProductSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -769,6 +780,7 @@ class OrderViewSet(EvibesViewSet):
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)}) return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)})
@action(detail=True, methods=["post"], url_path="bulk_add_order_products") @action(detail=True, methods=["post"], url_path="bulk_add_order_products")
@method_decorator(ratelimit(key="ip", rate="1/s" if not settings.DEBUG else "44/s"))
def bulk_add_order_products(self, request: Request, *args, **kwargs) -> Response: def bulk_add_order_products(self, request: Request, *args, **kwargs) -> Response:
serializer = BulkAddOrderProductsSerializer(data=request.data) serializer = BulkAddOrderProductsSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -788,6 +800,7 @@ class OrderViewSet(EvibesViewSet):
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)}) return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": str(ve)})
@action(detail=True, methods=["post"], url_path="bulk_remove_order_products") @action(detail=True, methods=["post"], url_path="bulk_remove_order_products")
@method_decorator(ratelimit(key="ip", rate="1/s" if not settings.DEBUG else "44/s"))
def bulk_remove_order_products(self, request: Request, *args, **kwargs) -> Response: def bulk_remove_order_products(self, request: Request, *args, **kwargs) -> Response:
serializer = BulkRemoveOrderProductsSerializer(data=request.data) serializer = BulkRemoveOrderProductsSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)

View file

@ -1,3 +1,4 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import OpenApiParameter from drf_spectacular.utils import OpenApiParameter
from engine.vibes_auth.messaging.serializers import ( from engine.vibes_auth.messaging.serializers import (

View file

@ -14,7 +14,6 @@ from django.contrib.auth import get_user_model
from django.db.models import Q from django.db.models import Q
from engine.vibes_auth.choices import SenderType from engine.vibes_auth.choices import SenderType
from engine.vibes_auth.messaging.services import send_message as svc_send_message
from engine.vibes_auth.models import ChatThread from engine.vibes_auth.models import ChatThread
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -97,6 +96,8 @@ def build_router() -> Optional["Router"]:
@router.message() @router.message()
async def any_message(message: types.Message): # type: ignore[valid-type] async def any_message(message: types.Message): # type: ignore[valid-type]
from engine.vibes_auth.messaging.services import send_message as svc_send_message
if not message.from_user or not message.text: if not message.from_user or not message.text:
return return
tid = message.from_user.id tid = message.from_user.id

View file

@ -113,7 +113,7 @@ SPECTACULAR_SETTINGS = {
"ENABLE_DJANGO_DEPLOY_CHECK": not DEBUG, # noqa: F405 "ENABLE_DJANGO_DEPLOY_CHECK": not DEBUG, # noqa: F405
"SWAGGER_UI_FAVICON_HREF": r"/static/favicon.png", "SWAGGER_UI_FAVICON_HREF": r"/static/favicon.png",
"SWAGGER_UI_SETTINGS": { "SWAGGER_UI_SETTINGS": {
"connectSocket": True, "connectSocket": False,
"socketMaxMessages": 8, "socketMaxMessages": 8,
"socketMessagesInitialOpened": False, "socketMessagesInitialOpened": False,
}, },