"""Routes available only to staff, including isolated message attachment delivery.""" import mimetypes from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied, ValidationError from django.http import FileResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect from django.views.decorators.cache import never_cache from django.views.decorators.http import require_GET, require_POST from apps.attachments.delivery import signed_media_redirect from apps.attachments.models import Attachment from .models import MessageShare, Ticket from .staff_conversation import received_messages, unread_shares, visible_messages from .sharing import mark_share_opened, revoke_staff_share from .views import staff_required def received_message_share(request, share_id): grant = get_object_or_404(MessageShare.objects.select_related("message__author__staff_profile", "granted_by__staff_profile"), public_id=share_id) profile = getattr(request.user, "staff_profile", None) if (grant.user_id != request.user.pk or grant.revoked_at or grant.message.deleted_at or not request.user.is_active or not profile or not profile.is_active): raise PermissionDenied return grant def submit_share_form(request, form, save, success_message): if not form.is_valid(): messages.error(request, "گیرنده، سطح دسترسی یا توضیح اشتراک معتبر نیست.") return try: save() except ValidationError as error: messages.error(request, " ".join(error.messages)) else: messages.success(request, success_message) @staff_required @require_GET @never_cache def inbox(request): from django.urls import reverse return redirect(reverse("staff_tickets:list") + "?tab=shared") @staff_required @require_POST def revoke_message(request, public_id, share_id): grant = get_object_or_404(MessageShare.objects.select_related("message__ticket"), public_id=share_id, message__ticket__public_id=public_id) revoke_staff_share(grant, request.user) messages.success(request, "دسترسی به پیام اشتراکی لغو شد؛ سابقه آن حفظ شده است.") return redirect("staff_tickets:detail", public_id=public_id) @staff_required @require_GET @never_cache def shared_message(request, share_id): grant = received_message_share(request, share_id) return redirect("staff_tickets:detail", public_id=grant.message.ticket.public_id) @staff_required @require_POST @never_cache def read_shared_messages(request, public_id): ticket = get_object_or_404(Ticket, public_id=public_id) allowed = visible_messages(ticket, request.user) ids = request.POST.getlist("message_ids")[:100] # Filter strings without parsing attacker-controlled UUIDs in the ORM. visible = {str(value) for value in allowed.values_list("public_id", flat=True)} accepted = set(ids) & visible grants = received_messages(request.user, ticket).filter(message__public_id__in=accepted) for grant in grants: if grant.first_viewed_at is None: mark_share_opened(grant, request.user) return JsonResponse({"unread_shares": unread_shares(request.user)}) @staff_required @require_GET @never_cache def unread_count(request): return JsonResponse({"unread_shares": unread_shares(request.user)}) @staff_required @require_GET @never_cache def shared_attachment(request, share_id, attachment_id, download=False): grant = received_message_share(request, share_id) attachment = get_object_or_404(Attachment, public_id=attachment_id, message_id=grant.message_id) if settings.USE_NGINX_MEDIA: return signed_media_redirect(attachment.relative_path, download=download) content_type = attachment.mime_type or mimetypes.guess_type(attachment.original_name)[0] or "application/octet-stream" response = FileResponse(attachment.file.open("rb"), content_type=content_type, as_attachment=download, filename=attachment.original_name) response["X-Content-Type-Options"] = "nosniff" return response