diff --git a/accounts/views.py b/accounts/views.py index 6a4f23c..beb49ff 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -8,7 +8,9 @@ from django import forms from django.contrib.auth.decorators import login_required from accounts.models import Profile from accounts.forms import CustomerForm +from processes.utils import scope_customers_queryset from common.consts import UserRoles +from common.decorators import allowed_roles # Create your views here. @@ -35,9 +37,11 @@ def dashboard(request): @login_required +@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT]) def customer_list(request): # Get all profiles that have customer role - customers = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user') + base = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user') + customers = scope_customers_queryset(request.user, base) form = CustomerForm() return render(request, "accounts/customer_list.html", { @@ -47,6 +51,8 @@ def customer_list(request): @require_POST +@login_required +@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT]) def add_customer_ajax(request): """AJAX endpoint for adding customers""" form = CustomerForm(request.POST, request.FILES) @@ -85,6 +91,8 @@ def add_customer_ajax(request): @require_POST +@login_required +@allowed_roles([UserRoles.ADMIN, UserRoles.BROKER, UserRoles.MANAGER, UserRoles.ACCOUNTANT]) def edit_customer_ajax(request, customer_id): customer = get_object_or_404(Profile, id=customer_id) form = CustomerForm(request.POST, request.FILES, instance=customer) @@ -122,6 +130,7 @@ def edit_customer_ajax(request, customer_id): }) @require_GET +@login_required def get_customer_data(request, customer_id): customer = get_object_or_404(Profile, id=customer_id) @@ -162,6 +171,7 @@ def get_customer_data(request, customer_id): }) +@login_required def logout_view(request): """Log out current user and redirect to login page.""" logout(request) diff --git a/certificates/views.py b/certificates/views.py index 8cfb704..ce6748e 100644 --- a/certificates/views.py +++ b/certificates/views.py @@ -12,6 +12,7 @@ from .models import CertificateTemplate, CertificateInstance from common.consts import UserRoles from _helpers.jalali import Gregorian +from processes.utils import get_scoped_instance_or_404 def _to_jalali(date_obj): @@ -46,7 +47,7 @@ def _render_template(template: CertificateTemplate, instance: ProcessInstance): @login_required def certificate_step(request, instance_id, step_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) # Ensure all previous steps are completed and invoice settled prior_steps = instance.process.steps.filter(order__lt=instance.current_step.order if instance.current_step else 9999) @@ -128,7 +129,7 @@ def certificate_step(request, instance_id, step_id): @login_required def certificate_print(request, instance_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) cert = CertificateInstance.objects.filter(process_instance=instance).order_by('-created').first() template = cert.template if cert else None return render(request, 'certificates/print.html', { diff --git a/common/decorators.py b/common/decorators.py index 7ac7259..7493abe 100644 --- a/common/decorators.py +++ b/common/decorators.py @@ -3,7 +3,7 @@ from functools import wraps from django.http import JsonResponse, HttpResponse from django.shortcuts import redirect -from extensions.consts import UserRoles +from common.consts import UserRoles def require_ajax(view_func): diff --git a/contracts/views.py b/contracts/views.py index 7a1788b..87c8ff2 100644 --- a/contracts/views.py +++ b/contracts/views.py @@ -11,6 +11,7 @@ from .models import ContractTemplate, ContractInstance from invoices.models import Invoice, Quote from _helpers.utils import jalali_converter2 from django.http import JsonResponse +from processes.utils import get_scoped_instance_or_404 def build_contract_context(instance: ProcessInstance) -> dict: @@ -52,7 +53,7 @@ def build_contract_context(instance: ProcessInstance) -> dict: @login_required def contract_step(request, instance_id, step_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) # Resolve step navigation step = get_object_or_404(instance.process.steps, id=step_id) previous_step = instance.process.steps.filter(order__lt=step.order).last() @@ -117,7 +118,7 @@ def contract_step(request, instance_id, step_id): @login_required def contract_print(request, instance_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) contract = get_object_or_404(ContractInstance, process_instance=instance) return render(request, 'contracts/contract_print.html', { 'instance': instance, diff --git a/db.sqlite3 b/db.sqlite3 index 2808c83..425dfa9 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/installations/views.py b/installations/views.py index ac27db9..008135b 100644 --- a/installations/views.py +++ b/installations/views.py @@ -10,10 +10,11 @@ from accounts.models import Role from invoices.models import Item, Quote, QuoteItem from .models import InstallationAssignment, InstallationReport, InstallationPhoto, InstallationItemChange from decimal import Decimal, InvalidOperation +from processes.utils import get_scoped_instance_or_404 @login_required def installation_assign_step(request, instance_id, step_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) previous_step = instance.process.steps.filter(order__lt=step.order).last() next_step = instance.process.steps.filter(order__gt=step.order).first() @@ -104,7 +105,7 @@ def create_item_changes_for_report(report, remove_map, add_map, quote_price_map) @login_required def installation_report_step(request, instance_id, step_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) previous_step = instance.process.steps.filter(order__lt=step.order).last() diff --git a/invoices/views.py b/invoices/views.py index 8820598..7e01a51 100644 --- a/invoices/views.py +++ b/invoices/views.py @@ -14,11 +14,15 @@ from accounts.models import Role from common.consts import UserRoles from .models import Item, Quote, QuoteItem, Payment, Invoice, InvoiceItem from installations.models import InstallationReport, InstallationItemChange - +from processes.utils import get_scoped_instance_or_404 @login_required def quote_step(request, instance_id, step_id): """مرحله انتخاب اقلام و ساخت پیش‌فاکتور""" + # Enforce scoped access to prevent URL tampering + instance = get_scoped_instance_or_404(request, instance_id) + + # Enforce scoped access to prevent URL tampering instance = get_object_or_404( ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'), id=instance_id @@ -68,7 +72,7 @@ def quote_step(request, instance_id, step_id): @login_required def create_quote(request, instance_id, step_id): """ساخت/بروزرسانی پیش‌فاکتور از اقلام انتخابی""" - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) # enforce permission: only BROKER can create/update quote profile = getattr(request.user, 'profile', None) @@ -219,6 +223,9 @@ def create_quote(request, instance_id, step_id): @login_required def quote_preview_step(request, instance_id, step_id): """مرحله صدور پیش‌فاکتور - نمایش و تایید فاکتور""" + # Enforce scoped access to prevent URL tampering + instance = get_scoped_instance_or_404(request, instance_id) + instance = get_object_or_404( ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile', 'broker', 'broker__company', 'broker__affairs', 'broker__affairs__county', 'broker__affairs__county__city'), id=instance_id @@ -261,7 +268,7 @@ def quote_preview_step(request, instance_id, step_id): @login_required def quote_print(request, instance_id): """صفحه پرینت پیش‌فاکتور""" - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) quote = get_object_or_404(Quote, process_instance=instance) return render(request, 'invoices/quote_print.html', { @@ -274,7 +281,7 @@ def quote_print(request, instance_id): @login_required def approve_quote(request, instance_id, step_id): """تایید پیش‌فاکتور و انتقال به مرحله بعدی""" - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) quote = get_object_or_404(Quote, process_instance=instance) # enforce permission: only BROKER can approve @@ -316,6 +323,9 @@ def approve_quote(request, instance_id, step_id): @login_required def quote_payment_step(request, instance_id, step_id): """مرحله سوم: ثبت فیش‌های واریزی پیش‌فاکتور""" + # Enforce scoped access to prevent URL tampering + instance = get_scoped_instance_or_404(request, instance_id) + instance = get_object_or_404( ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'), id=instance_id @@ -449,7 +459,7 @@ def quote_payment_step(request, instance_id, step_id): @login_required def add_quote_payment(request, instance_id, step_id): """افزودن فیش واریزی جدید برای پیش‌فاکتور""" - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) quote = get_object_or_404(Quote, process_instance=instance) invoice, _ = Invoice.objects.get_or_create( @@ -564,7 +574,7 @@ def add_quote_payment(request, instance_id, step_id): @require_POST @login_required def delete_quote_payment(request, instance_id, step_id, payment_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) quote = get_object_or_404(Quote, process_instance=instance) invoice = Invoice.objects.filter(quote=quote).first() @@ -632,6 +642,9 @@ def delete_quote_payment(request, instance_id, step_id, payment_id): @login_required def final_invoice_step(request, instance_id, step_id): """تجمیع اقلام پیش‌فاکتور با تغییرات نصب و صدور فاکتور نهایی""" + # Enforce scoped access to prevent URL tampering + instance = get_scoped_instance_or_404(request, instance_id) + instance = get_object_or_404( ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'), id=instance_id @@ -770,7 +783,7 @@ def final_invoice_step(request, instance_id, step_id): @login_required def final_invoice_print(request, instance_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) invoice = get_object_or_404(Invoice, process_instance=instance) items = invoice.items.select_related('item').filter(is_deleted=False).all() return render(request, 'invoices/final_invoice_print.html', { @@ -783,7 +796,7 @@ def final_invoice_print(request, instance_id): @require_POST @login_required def approve_final_invoice(request, instance_id, step_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) invoice = get_object_or_404(Invoice, process_instance=instance) # only MANAGER can approve @@ -811,7 +824,7 @@ def approve_final_invoice(request, instance_id, step_id): @login_required def add_special_charge(request, instance_id, step_id): """افزودن هزینه ویژه تعمیر/تعویض به فاکتور نهایی به‌صورت آیتم جداگانه""" - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) invoice = get_object_or_404(Invoice, process_instance=instance) # only MANAGER can add special charges try: @@ -848,7 +861,7 @@ def add_special_charge(request, instance_id, step_id): @require_POST @login_required def delete_special_charge(request, instance_id, step_id, item_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) invoice = get_object_or_404(Invoice, process_instance=instance) # only MANAGER can delete special charges try: @@ -870,7 +883,7 @@ def delete_special_charge(request, instance_id, step_id, item_id): @login_required def final_settlement_step(request, instance_id, step_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) if not instance.can_access_step(step): @@ -976,7 +989,7 @@ def final_settlement_step(request, instance_id, step_id): @require_POST @login_required def add_final_payment(request, instance_id, step_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) invoice = get_object_or_404(Invoice, process_instance=instance) # Only BROKER can add final settlement payments @@ -1093,7 +1106,7 @@ def add_final_payment(request, instance_id, step_id): @require_POST @login_required def delete_final_payment(request, instance_id, step_id, payment_id): - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) step = get_object_or_404(instance.process.steps, id=step_id) invoice = get_object_or_404(Invoice, process_instance=instance) payment = get_object_or_404(Payment, id=payment_id, invoice=invoice) diff --git a/processes/templates/processes/request_list.html b/processes/templates/processes/request_list.html index feb1294..611a8de 100644 --- a/processes/templates/processes/request_list.html +++ b/processes/templates/processes/request_list.html @@ -1,5 +1,6 @@ {% extends '_base.html' %} {% load static %} +{% load accounts_tags %} {% block sidebar %} {% include 'sidebars/admin.html' %} @@ -43,10 +44,12 @@ + {% if request.user|is_broker %} + {% endif %} @@ -132,6 +135,91 @@ + {% if access_denied %} + + {% endif %} + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + حذف فیلتر + +
+
+
+
@@ -178,7 +266,7 @@ - + {% empty %} - + + + + + + + + + + + {% endfor %} diff --git a/processes/templatetags/processes_tags.py b/processes/templatetags/processes_tags.py index 0922e3c..5ee7b2f 100644 --- a/processes/templatetags/processes_tags.py +++ b/processes/templatetags/processes_tags.py @@ -1,6 +1,7 @@ from django import template from django.utils.safestring import mark_safe from ..models import ProcessInstance, StepInstance +from ..utils import count_incomplete_instances register = template.Library() @@ -104,3 +105,8 @@ def instance_info(instance, modal_id=None): title="اطلاعات کامل چاه و نماینده"> ''' return mark_safe(html) + + +@register.simple_tag +def incomplete_requests_count(user): + return count_incomplete_instances(user) diff --git a/processes/utils.py b/processes/utils.py new file mode 100644 index 0000000..951398e --- /dev/null +++ b/processes/utils.py @@ -0,0 +1,118 @@ +from django.shortcuts import get_object_or_404 +from .models import ProcessInstance +from common.consts import UserRoles + + +def scope_instances_queryset(user, queryset=None): + """Return a queryset of ProcessInstance scoped by the user's role. + + If no profile/role, returns an empty queryset. + """ + qs = queryset if queryset is not None else ProcessInstance.objects.all() + profile = getattr(user, 'profile', None) + if not profile: + return qs.none() + try: + if profile.has_role(UserRoles.INSTALLER): + # Only instances assigned to this installer + from installations.models import InstallationAssignment + assign_ids = InstallationAssignment.objects.filter(installer=user).values_list('process_instance', flat=True) + return qs.filter(id__in=assign_ids) + if profile.has_role(UserRoles.BROKER): + return qs.filter(broker=profile.broker) + if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER): + return qs.filter(broker__affairs__county=profile.county) + if profile.has_role(UserRoles.ADMIN): + return qs + # if profile.has_role(UserRoles.WATER_RESOURCE_MANAGER) or profile.has_role(UserRoles.HEADQUARTER): + # return qs.filter(well__county=profile.county) + # Fallback: no special scope + # return qs + except Exception: + return qs.none() + + +def count_incomplete_instances(user): + """Count non-completed, non-deleted requests within the user's scope.""" + base = ProcessInstance.objects.select_related('well').filter(is_deleted=False).exclude(status='completed') + return scope_instances_queryset(user, base).count() + + +def user_can_access_instance(user, instance: ProcessInstance) -> bool: + """Check if user can access a specific instance based on scoping rules.""" + try: + scoped = scope_instances_queryset(user, ProcessInstance.objects.filter(id=instance.id)) + return scoped.exists() + except Exception: + return False + + +def get_scoped_instance_or_404(request, instance_id: int) -> ProcessInstance: + """Return instance only if it's within the user's scope; otherwise 404. + + Use this in any view receiving instance_id from URL to prevent URL tampering. + """ + base = ProcessInstance.objects.filter(is_deleted=False) + qs = scope_instances_queryset(request.user, base) + return get_object_or_404(qs, id=instance_id) + + +def scope_wells_queryset(user, queryset=None): + """Return a queryset of Well scoped by the user's role (parity with instances).""" + try: + from wells.models import Well + qs = queryset if queryset is not None else Well.objects.all() + profile = getattr(user, 'profile', None) + if not profile: + return qs.none() + if profile.has_role(UserRoles.ADMIN): + return qs + if profile.has_role(UserRoles.BROKER): + return qs.filter(broker=profile.broker) + if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER): + return qs.filter(broker__affairs__county=profile.county) + if profile.has_role(UserRoles.INSTALLER): + # Wells that have instances assigned to this installer + from installations.models import InstallationAssignment + assign_ids = InstallationAssignment.objects.filter(installer=user).values_list('process_instance', flat=True) + inst_qs = ProcessInstance.objects.filter(id__in=assign_ids) + return qs.filter(process_instances__in=inst_qs).distinct() + # Fallback + return qs.none() + except Exception: + return qs.none() if 'qs' in locals() else [] + + +def scope_customers_queryset(user, queryset=None): + """Return a queryset of customer Profiles scoped by user's role. + + Assumes queryset is Profiles already filtered to customers, otherwise we filter here. + """ + try: + from accounts.models import Profile + qs = queryset if queryset is not None else Profile.objects.all() + # Ensure we're only looking at customer profiles + from common.consts import UserRoles as UR + qs = qs.filter(roles__slug=UR.CUSTOMER.value, is_deleted=False) + + profile = getattr(user, 'profile', None) + if not profile: + return qs.none() + if profile.has_role(UserRoles.ADMIN): + return qs + if profile.has_role(UserRoles.BROKER): + return qs.filter(broker=profile.broker) + if profile.has_role(UserRoles.ACCOUNTANT) or profile.has_role(UserRoles.MANAGER): + return qs.filter(county=profile.county) + if profile.has_role(UserRoles.INSTALLER): + # Customers that are representatives of instances assigned to this installer + from installations.models import InstallationAssignment + assign_ids = InstallationAssignment.objects.filter(installer=user).values_list('process_instance', flat=True) + rep_ids = ProcessInstance.objects.filter(id__in=assign_ids).values_list('representative', flat=True) + return qs.filter(user_id__in=rep_ids) + # Fallback + return qs.none() + except Exception: + return qs.none() if 'qs' in locals() else [] + + diff --git a/processes/views.py b/processes/views.py index f84e826..38ad946 100644 --- a/processes/views.py +++ b/processes/views.py @@ -7,19 +7,62 @@ from django.http import JsonResponse from django.views.decorators.http import require_POST, require_GET from django.db import transaction from django.contrib.auth import get_user_model -from .models import Process, ProcessInstance, StepInstance +from .models import Process, ProcessInstance, StepInstance, ProcessStep +from .utils import scope_instances_queryset, get_scoped_instance_or_404 +from installations.models import InstallationAssignment from wells.models import Well -from accounts.models import Profile +from accounts.models import Profile, Broker +from locations.models import Affairs from accounts.forms import CustomerForm from wells.forms import WellForm from wells.models import WaterMeterManufacturer +from common.consts import UserRoles @login_required def request_list(request): """نمایش لیست درخواست‌ها با جدول و مدال ایجاد""" - instances = ProcessInstance.objects.select_related('well', 'representative', 'requester').prefetch_related('step_instances__step').filter(is_deleted=False).order_by('-created') + instances = ProcessInstance.objects.select_related('well', 'representative', 'requester', 'broker', 'current_step', 'process').prefetch_related('step_instances__step').filter(is_deleted=False).order_by('-created') + access_denied = False + + # filter by roles (scoped queryset) + try: + instances = scope_instances_queryset(request.user, instances) + if not instances.exists() and not getattr(request.user, 'profile', None): + access_denied = True + instances = instances.none() + except Exception: + access_denied = True + instances = instances.none() + + # Filters + status_q = (request.GET.get('status') or '').strip() + affairs_q = (request.GET.get('affairs') or '').strip() + broker_q = (request.GET.get('broker') or '').strip() + step_q = (request.GET.get('step') or '').strip() + + if status_q: + instances = instances.filter(status=status_q) + if affairs_q: + try: + instances = instances.filter(well__affairs_id=int(affairs_q)) + except Exception: + pass + if broker_q: + try: + instances = instances.filter(broker_id=int(broker_q)) + except Exception: + pass + if step_q: + try: + instances = instances.filter(current_step_id=int(step_q)) + except Exception: + pass processes = Process.objects.filter(is_active=True) + status_choices = list(ProcessInstance.STATUS_CHOICES) + affairs_list = Affairs.objects.all().order_by('name') + brokers_list = Broker.objects.all().order_by('name') + steps_list = ProcessStep.objects.select_related('process').all().order_by('process__name', 'order') manufacturers = WaterMeterManufacturer.objects.all().order_by('name') # Calculate progress for each instance @@ -52,6 +95,16 @@ def request_list(request): 'completed_count': completed_count, 'in_progress_count': in_progress_count, 'pending_count': pending_count, + # filter context + 'status_choices': status_choices, + 'affairs_list': affairs_list, + 'brokers_list': brokers_list, + 'steps_list': steps_list, + 'filter_status': status_q, + 'filter_affairs': affairs_q, + 'filter_broker': broker_q, + 'filter_step': step_q, + 'access_denied': access_denied, }) @@ -125,6 +178,13 @@ def lookup_representative_by_national_code(request): def create_request_with_entities(request): """ایجاد/به‌روزرسانی چاه و نماینده و سپس ایجاد درخواست""" User = get_user_model() + # Only BROKER can create requests + try: + if not (hasattr(request.user, 'profile') and request.user.profile.has_role(UserRoles.BROKER)): + return JsonResponse({'ok': False, 'error': 'فقط کارگزار مجاز به ایجاد درخواست است'}, status=403) + except Exception: + return JsonResponse({'ok': False, 'error': 'فقط کارگزار مجاز به ایجاد درخواست است'}, status=403) + process_id = request.POST.get('process') process = Process.objects.get(id=process_id) description = request.POST.get('description', '') @@ -230,6 +290,14 @@ def create_request_with_entities(request): well.broker = current_profile.broker well.save() + # Ensure no active (non-deleted, non-completed) request exists for this well + try: + active_exists = ProcessInstance.objects.filter(well=well, is_deleted=False).exclude(status='completed').exists() + if active_exists: + return JsonResponse({'ok': False, 'error': 'برای این چاه یک درخواست جاری وجود دارد. ابتدا آن را تکمیل یا حذف کنید.'}, status=400) + except Exception: + return JsonResponse({'ok': False, 'error': 'خطا در بررسی وضعیت درخواست‌های قبلی این چاه'}, status=400) + # Create request instance instance = ProcessInstance.objects.create( process=process, @@ -261,7 +329,17 @@ def create_request_with_entities(request): @login_required def delete_request(request, instance_id): """حذف درخواست""" - instance = get_object_or_404(ProcessInstance, id=instance_id) + instance = get_scoped_instance_or_404(request, instance_id) + # Only BROKER can delete requests and only within their scope + try: + profile = getattr(request.user, 'profile', None) + if not (profile and profile.has_role(UserRoles.BROKER)): + return JsonResponse({'success': False, 'message': 'فقط کارگزار مجاز به حذف درخواست است'}, status=403) + # Enforce ownership by broker (prevent deleting others' requests) + if instance.broker_id and profile.broker and instance.broker_id != profile.broker.id: + return JsonResponse({'success': False, 'message': 'شما مجاز به حذف این درخواست نیستید'}, status=403) + except Exception: + return JsonResponse({'success': False, 'message': 'فقط کارگزار مجاز به حذف درخواست است'}, status=403) code = instance.code if instance.status == 'completed': return JsonResponse({ @@ -278,10 +356,10 @@ def delete_request(request, instance_id): @login_required def step_detail(request, instance_id, step_id): """نمایش جزئیات مرحله خاص""" - instance = get_object_or_404( - ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile'), - id=instance_id - ) + # Enforce scoped access to prevent URL tampering + instance = get_scoped_instance_or_404(request, instance_id) + # Prefetch for performance + instance = ProcessInstance.objects.select_related('process', 'well', 'requester', 'representative', 'representative__profile').get(id=instance.id) step = get_object_or_404(instance.process.steps, id=step_id) # If the request is already completed, redirect to read-only summary page if instance.status == 'completed': @@ -339,7 +417,8 @@ def step_detail(request, instance_id, step_id): @login_required def instance_steps(request, instance_id): """هدایت به مرحله فعلی instance""" - instance = get_object_or_404(ProcessInstance, id=instance_id) + # Enforce scoped access to prevent URL tampering + instance = get_scoped_instance_or_404(request, instance_id) if not instance.current_step: # اگر مرحله فعلی تعریف نشده، به اولین مرحله برو @@ -361,6 +440,9 @@ def instance_steps(request, instance_id): @login_required def instance_summary(request, instance_id): """نمای خلاصهٔ فقط‌خواندنی برای درخواست‌های تکمیل‌شده.""" + # Enforce scoped access to prevent URL tampering + instance = get_scoped_instance_or_404(request, instance_id) + instance = get_object_or_404(ProcessInstance.objects.select_related('well', 'representative'), id=instance_id) # Only show for completed requests; otherwise route to steps if instance.status != 'completed': diff --git a/templates/sidebars/admin.html b/templates/sidebars/admin.html index aacdf24..066c177 100644 --- a/templates/sidebars/admin.html +++ b/templates/sidebars/admin.html @@ -1,4 +1,5 @@ {% load static %} +{% load accounts_tags %}
{{ item.instance.get_status_display_with_color|safe }}{{ item.instance.jcreated }}{{ item.instance.jcreated_date }}
@@ -196,19 +284,31 @@ {% endif %} + {% if request.user|is_broker %}
  • حذف
  • + {% endif %}
    موردی ثبت نشده استموردی ثبت نشده است