From b425caf22f0c612a4ed014b29ec4e4f7b761bc69 Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Sat, 26 Jul 2025 15:50:57 +0330 Subject: [PATCH] add pos header validation middleware --- apps/pos_device/middlewares.py | 30 +++++ .../api/v1/validators/header_validation.py | 108 ++++++++++-------- apps/product/web/api/v1/viewsets/quota_api.py | 3 +- .../api/v1/viewsets/quota_distribution_api.py | 36 ++++-- 4 files changed, 114 insertions(+), 63 deletions(-) create mode 100644 apps/pos_device/middlewares.py diff --git a/apps/pos_device/middlewares.py b/apps/pos_device/middlewares.py new file mode 100644 index 0000000..63f6105 --- /dev/null +++ b/apps/pos_device/middlewares.py @@ -0,0 +1,30 @@ +from rest_framework.exceptions import APIException +from django.utils.timezone import now + +from apps.pos_device.models import DeviceVersion, ProviderCompany, Sessions + + +class POSDeviceMiddleware: + REQUIRED_HEADERS = [ + 'device-id', 'device-mac', 'device-serial', 'device-name', + 'device-sdk', 'device-provider', 'device-version', + 'device-vname', 'device-lng', 'device-lot' # noqa + ] + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request, *args, **kwargs): + pass + + def is_post_request(self, request): # noqa + """ check if is pos request """ + + has_device_headers = request.headers.get('device-id') and request.headers.get('device-mac') + is_pos_api_path = request.path.startswith('/api/pos/') + return has_device_headers or is_pos_api_path + + def validate_pos_request(self, request): + """ validate request headers from pos device """ + + data = {key: request.headers.get(key) for key in self.REQUIRED_HEADERS} diff --git a/apps/pos_device/web/api/v1/validators/header_validation.py b/apps/pos_device/web/api/v1/validators/header_validation.py index cf7cc24..0b063ea 100644 --- a/apps/pos_device/web/api/v1/validators/header_validation.py +++ b/apps/pos_device/web/api/v1/validators/header_validation.py @@ -1,66 +1,74 @@ import datetime from django.http import JsonResponse +from django.utils.timezone import now +from rest_framework.exceptions import APIException from apps.pos_device.models import Device, ProviderCompany, Sessions, DeviceVersion def get_client_ip(request): - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - # اگر از پروکسی استفاده می‌شود، IP اول را برمی‌گردانیم - ip = x_forwarded_for.split(',')[0] - else: - # در غیر این صورت از REMOTE_ADDR استفاده می‌کنیم - ip = request.META.get('REMOTE_ADDR') - return ip + forwarded = request.META.get('HTTP_X_FORWARDED_FOR') + return forwarded.split(',')[0] if forwarded else request.META.get('REMOTE_ADDR') -class PosDeviceValidator: +class DeviceValidator: + REQUIRED_HEADERS = [ + 'device-id', 'device-mac', 'device-serial', 'device-name', + 'device-sdk', 'device-provider', 'device-version', + 'device-vname', 'device-lng', 'device-lot' # noqa + ] + def __init__(self, request): self.request = request self.headers = request.headers - self.device_id = request.headers.get('device-id') - self.device_mac = request.headers.get('device-mac') - self.device_name = request.headers.get('device-name') - self.device_sdk = request.headers.get('device-sdk') - self.device_serial = request.headers.get('device-serial') - self.device_provider = request.headers.get('device-provider') - self.device_version = request.headers.get('device-version') - self.device_version_name = request.headers.get('device-vname') - self.device_lng = request.headers.get('device-lng') - self.device_lot = request.headers.get('device-lot') + self.data = {key: self.headers.get(key) for key in self.REQUIRED_HEADERS} + self.errors = [] - def validation_version(self): - if self.device_provider == "" or self.device_provider == None: - return JsonResponse({'result': 'پارامتر های ارسالی صحیح نمیباشد!'}, status=402) - - company = ProviderCompany.objects.filter(en_name=self.device_provider).first() - if not company: - return JsonResponse({'result': 'شرکت پرداخت الکترونیک پشتیبانی نمیشود!'}, status=402) - - if not company.active: - return JsonResponse({'result': 'شرکت پرداخت الکترونیک توسط مدیریت مسدود شده است!'}, status=402) - version = DeviceVersion.objects.filter(company=company).order_by('code') - if not version: - return JsonResponse({'result': ' هیچ نسخه معتبری برای این شرکت پرداخت الکترونیک منتشر نشده است!'}, - status=402) - - current_version = version.filter(code=self.device_version).first() - if not current_version or current_version.remove: - return JsonResponse({'result': f'نسخه {self.device_version_name} منقضی شده است لطفا بروز رسانی کنید '}, - status=402) + def validate_required_headers(self): + missing = [key for key, value in self.data.items() if not value] + if missing: + raise APIException( + f'پارامترهای ارسالی ناقص هستند: {", ".join(missing)}', # noqa + code=400 + ) return None - def validation_device(self): - pos_session = Sessions.objects.filter(pos__pos_id=self.device_id, mac=self.device_mac).first() - if not pos_session: - return None - else: - pos_session.session_last_seen_date = datetime.datetime.now() - pos_session.lng = self.device_lng - pos_session.lot = self.device_lot - pos_session.version = self.device_version - pos_session.ip = get_client_ip(self.request) - pos_session.save() - return pos_session.pos.pos_id \ No newline at end of file + def validate_version(self): + provider_name = self.data['device-provider'] + company = ProviderCompany.objects.filter(en_name=provider_name).first() + + if not company: + raise APIException('شرکت پرداخت الکترونیک پشتیبانی نمی‌شود!', code=402) # noqa + + if not company.active: + raise APIException('شرکت پرداخت الکترونیک مسدود شده است!', code=402) # noqa + + versions = DeviceVersion.objects.filter(company=company) + if not versions.exists(): + raise APIException('هیچ نسخه‌ای برای این شرکت ثبت نشده است!', code=402) # noqa + + current_version = versions.filter(code=self.data['device-version']).first() + if not current_version or current_version.remove: + raise APIException( + f'نسخه {self.data["device-vname"]} منقضی شده است. لطفا بروزرسانی کنید.', # noqa + code=402 + ) + return None + + def validate_device_session(self): + session = Sessions.objects.filter( + pos__pos_id=self.data['device-id'], + mac=self.data['device-mac'] + ).first() + + if session: + session.session_last_seen_date = now() + session.lng = self.data['device-lng'] + session.lot = self.data['device-lot'] + session.version = self.data['device-version'] + session.ip = get_client_ip(self.request) + session.save() + return session.pos.pos_id + + return None diff --git a/apps/product/web/api/v1/viewsets/quota_api.py b/apps/product/web/api/v1/viewsets/quota_api.py index a43e619..5b0ebf9 100644 --- a/apps/product/web/api/v1/viewsets/quota_api.py +++ b/apps/product/web/api/v1/viewsets/quota_api.py @@ -331,10 +331,9 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa # paginate queryset page = self.paginate_queryset( self.queryset.filter( - Q(assigned_organizations=organization) | Q(registerer_organization=organization), Q(is_closed=True) - ) + ).order_by('-modify_date') ) if page is not None: serializer = self.get_serializer(page, many=True) diff --git a/apps/product/web/api/v1/viewsets/quota_distribution_api.py b/apps/product/web/api/v1/viewsets/quota_distribution_api.py index 575be3b..6216736 100644 --- a/apps/product/web/api/v1/viewsets/quota_distribution_api.py +++ b/apps/product/web/api/v1/viewsets/quota_distribution_api.py @@ -1,6 +1,7 @@ from django.db.models import Q from apps.product.web.api.v1.serializers import quota_distribution_serializers as distribution_serializers +from apps.core.pagination import CustomPageNumberPagination from rest_framework.exceptions import APIException from apps.product import models as product_models from rest_framework.response import Response @@ -9,6 +10,8 @@ from rest_framework import viewsets, filters from rest_framework import status from django.db import transaction +from common.helpers import get_organization_by_user + def trash(queryset, pk): # noqa """ sent object to trash """ @@ -30,6 +33,7 @@ class QuotaDistributionViewSet(viewsets.ModelViewSet): serializer_class = distribution_serializers.QuotaDistributionSerializer filter_backends = [filters.SearchFilter] search_fields = [''] + CustomPageNumberPagination.page_size = 5 @transaction.atomic def create(self, request, *args, **kwargs): @@ -68,17 +72,27 @@ class QuotaDistributionViewSet(viewsets.ModelViewSet): return Response(serializer.data) - # @action( - # methods=['get'], - # url_name='my_distributions', - # url_path='my_distributions', - # name='my_distributions' - # ) - # def my_distributions(self, request): - # - # distributions = self.queryset.filter( - # Q(assigned_organization='') - # ) + @action( + methods=['get'], + detail=False, + url_name='my_distributions', + url_path='my_distributions', + name='my_distributions' + ) + def my_distributions(self, request): + """ list of my distributions """ + organization = get_organization_by_user(request.user) + + # paginate queryset + page = self.paginate_queryset( + self.queryset.filter( + Q(assigned_organization=organization) | + Q(assigner_organization=organization) + ).order_by('-modify_date') + ) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) @action( methods=['put'],