The Swagger header was a list of features. That is what the project does, not what a caller needs. It now opens with the three things anyone integrating against this API asks first: how do I get a token, what does an error look like, and when do I get rate limited -- plus the field-naming and date conventions, and a note that the schema is the contract and clients should be generated from it rather than written from memory. Swagger UI and Redoc both render Markdown, so it is structured rather than one long paragraph, which also fixes how badly the old mixed Persian/Latin line wrapped right-to-left. Two defects surfaced while checking the rendered schema: - /api/health was absent from the schema entirely. The trailing-slash alias was excluded with extend_schema(exclude=True)(HealthView), but that decorator mutates the class, and both routes pointed at the same class, so the canonical route disappeared with it. A subclass now carries the exclusion, and the endpoint is back, exactly once. Endpoint count is 56, not 55. - One endpoint was tagged 'محتوا (content)' while every other content endpoint used 'Curriculum (content)', which split the group into two sections. Tags are now declared with descriptions and ordered by the path a real user takes, rather than alphabetically. Comment pass over the codebase, keeping the substance and dropping the noise: the "⚠️ اضافه شده" prefixes dated comments against a moment no current reader shares; "فاز ۲ / بخش ۴" referenced a plan that no longer exists and only repeated the app name on the next line; "فاز اول - ۴ از ۱۱ اپ" had drifted (there are fourteen). INSTALLED_APPS is now grouped by responsibility. The TEST_LOGIN_PHONES block in settings shrank from 29 lines to 8 now that the same explanation lives in docs/operations.md and .env.example -- a comment beside the code should carry what a code reader needs, not a copy of the runbook. Checked mechanically that no comment references an identifier that no longer exists: zero. The dated measurements in rag/services/ingest.py and generate_questions_from_chunks.py were deliberately left alone -- a measurement carrying the date it was taken is the useful kind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
185 lines
8.6 KiB
Python
185 lines
8.6 KiB
Python
from django.shortcuts import get_object_or_404
|
|
from drf_spectacular.utils import extend_schema
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.throttling import ScopedRateThrottle
|
|
from rest_framework.views import APIView
|
|
|
|
from .models import BugReport, Ticket, TicketMessage
|
|
from .serializers import (
|
|
BugReportCreateRequestSerializer,
|
|
BugReportCreateResponseSerializer,
|
|
TicketCreateRequestSerializer,
|
|
TicketCreateResponseSerializer,
|
|
TicketDetailResponseSerializer,
|
|
TicketMessageCreateRequestSerializer,
|
|
TicketMessageCreateResponseSerializer,
|
|
)
|
|
|
|
|
|
class GuestAwareSupportThrottle(ScopedRateThrottle):
|
|
"""
|
|
همان مشکلِ GuestAwareAIThrottle (ai_tutor/views.py)،
|
|
GuestAwareFamilyThrottle (family/views.py) و GuestAwareDiscountThrottle/
|
|
GuestAwarePurchaseThrottle (discount/views.py، subscription/views.py)
|
|
اینجا هم هست. ScopedRateThrottle استاندارد برای درخواستِ
|
|
احرازهویتشده از request.user.pk بهعنوانِ کلیدِ throttle استفاده
|
|
میکنه؛ چون GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ
|
|
تازه (pk جدید) میسازه، یک مهاجم میتونست با ساختنِ پیدرپیِ حسابهای
|
|
مهمان، برای هر حساب یک bucket تازه از throttle_scope 'support_create'
|
|
بگیره. چون هر سه Endpointِ این اپ (BugReportCreateView،
|
|
TicketCreateView، TicketMessageCreateView) این scope رو بهاشتراک
|
|
میذارن و هرکدوم واقعاً یک رکورد در دیتابیس میسازن (BugReport،
|
|
Ticket، یا TicketMessage)، چرخشِ حساب مهمان میتونست محدودیتِ
|
|
۳/ساعتِ در نظر گرفتهشده رو چند برابر کنه و باعثِ spam بشه.
|
|
|
|
این کلاسِ throttle یکبار اینجا تعریف شده و بینِ هر سه Endpoint
|
|
بهاشتراک گذاشته میشه (بهجایِ تکرارِ یک کلاسِ مشابه برای هرکدوم)،
|
|
دقیقاً چون هر سه از یک throttle_scope مشترک استفاده میکنن. فقط
|
|
برای کاربرهای مهمان (is_guest=True) کلیدِ cache از pk کاربر به IP
|
|
کلاینت (get_ident) تغییر میکنه، پس همهی حسابهای مهمانِ ساختهشده
|
|
از یک IP یک bucket مشترک دارن و چرخشِ حساب دیگه bucket تازه نمیده.
|
|
رفتارِ throttle برای کاربرهای غیرمهمان (ثبتنامشده) دقیقاً مثلِ قبل
|
|
(کلید بر اساسِ pk) باقی میمونه؛ منطقِ ساختِ رکورد/ownership/serializer
|
|
در هر سه ویو دستنخوردهست.
|
|
"""
|
|
|
|
def get_cache_key(self, request, view):
|
|
user = request.user
|
|
if user and user.is_authenticated and getattr(user, 'is_guest', False):
|
|
ident = f'guest-ip:{self.get_ident(request)}'
|
|
elif user and user.is_authenticated:
|
|
ident = user.pk
|
|
else:
|
|
ident = self.get_ident(request)
|
|
|
|
return self.cache_format % {'scope': self.scope, 'ident': ident}
|
|
|
|
|
|
class BugReportCreateView(APIView):
|
|
"""POST /api/support/bug-reports — ثبت گزارش باگ"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
# بدونِ idempotency/throttle، یک کاربر میتونست تعدادِ نامحدودی BugReport
|
|
# بسازه؛ GuestAwareSupportThrottle برای مهمانها کلید رو بر اساسِ IP
|
|
# میسازه تا چرخشِ حساب bucket تازه نده.
|
|
throttle_scope = 'support_create'
|
|
throttle_classes = [GuestAwareSupportThrottle]
|
|
|
|
@extend_schema(
|
|
request=BugReportCreateRequestSerializer,
|
|
responses=BugReportCreateResponseSerializer,
|
|
tags=['Support (support)'],
|
|
)
|
|
def post(self, request):
|
|
serializer = BugReportCreateRequestSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
v = serializer.validated_data
|
|
|
|
bug = BugReport.objects.create(
|
|
user_id=request.user.id,
|
|
title=v['title'],
|
|
description=v['description'],
|
|
screenshot_url=v.get('screenshotUrl', ''),
|
|
)
|
|
|
|
data = {'bugReportId': str(bug.pk), 'status': bug.status}
|
|
return Response(BugReportCreateResponseSerializer(data).data)
|
|
|
|
|
|
class TicketCreateView(APIView):
|
|
"""POST /api/support/tickets — ایجاد تیکت"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
# مثلِ BugReportCreateView: هیچ idempotency ای نیست، پس این endpoint هم
|
|
# همون scope=support_create رو بهاشتراک میذاره تا محدودیتِ ساختِ
|
|
# رکوردهای پشتیبانی، مستقل از اینکه کدوم نوع رکورده، رعایت بشه.
|
|
# ⚠️ اصلاح شده: از همون GuestAwareSupportThrottleِ مشترک استفاده میشه
|
|
# تا چرخشِ حساب مهمان bucket تازه ندهد.
|
|
throttle_scope = 'support_create'
|
|
throttle_classes = [GuestAwareSupportThrottle]
|
|
|
|
@extend_schema(
|
|
request=TicketCreateRequestSerializer,
|
|
responses=TicketCreateResponseSerializer,
|
|
tags=['Support (support)'],
|
|
)
|
|
def post(self, request):
|
|
serializer = TicketCreateRequestSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
v = serializer.validated_data
|
|
|
|
ticket = Ticket.objects.create(
|
|
user_id=request.user.id,
|
|
subject=v['subject'],
|
|
category=v['category'],
|
|
priority=v.get('priority', 'normal'),
|
|
)
|
|
|
|
data = {'ticketId': str(ticket.pk), 'status': ticket.status}
|
|
return Response(TicketCreateResponseSerializer(data).data)
|
|
|
|
|
|
class TicketDetailView(APIView):
|
|
"""
|
|
GET /api/support/tickets/{id} — جزئیات و پیامهای تیکت
|
|
|
|
فقط تیکت متعلق به همون کاربر برمیگرده (فیلتر با user_id، دقیقاً
|
|
همون الگوی محدودسازی به مالک که در progress/views.py برای
|
|
UserStats و در ai_tutor/views.py برای Conversation استفاده شده).
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
@extend_schema(responses=TicketDetailResponseSerializer, tags=['Support (support)'])
|
|
def get(self, request, ticket_id):
|
|
ticket = get_object_or_404(Ticket, pk=ticket_id, user_id=request.user.id)
|
|
|
|
messages = [
|
|
{'id': str(m.pk), 'sender': m.sender, 'text': m.text}
|
|
for m in ticket.messages.all()
|
|
]
|
|
data = {
|
|
'ticketId': str(ticket.pk),
|
|
'subject': ticket.subject,
|
|
'status': ticket.status,
|
|
'messages': messages,
|
|
}
|
|
return Response(TicketDetailResponseSerializer(data).data)
|
|
|
|
|
|
class TicketMessageCreateView(APIView):
|
|
"""
|
|
POST /api/support/tickets/{id}/messages — ارسال پیام در تیکت
|
|
|
|
این Endpoint سمت کاربره، پس sender همیشه 'user' ثبت میشه. پاسخ
|
|
پشتیبانی (sender='support') از پنل ادمین ثبت میشه و طبق سند معماری
|
|
از همونجا رویداد TicketReplied برای Notifications ساطع میشه
|
|
(پیادهسازی در support/admin.py، نه اینجا).
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
# مثلِ دو Endpoint بالا: بدونِ throttle، یک کاربر میتونست تعدادِ
|
|
# نامحدودی پیام در یک تیکت بسازه.
|
|
# ⚠️ اصلاح شده: از همون GuestAwareSupportThrottleِ مشترک استفاده میشه
|
|
# تا چرخشِ حساب مهمان bucket تازه ندهد.
|
|
throttle_scope = 'support_create'
|
|
throttle_classes = [GuestAwareSupportThrottle]
|
|
|
|
@extend_schema(
|
|
request=TicketMessageCreateRequestSerializer,
|
|
responses=TicketMessageCreateResponseSerializer,
|
|
tags=['Support (support)'],
|
|
)
|
|
def post(self, request, ticket_id):
|
|
ticket = get_object_or_404(Ticket, pk=ticket_id, user_id=request.user.id)
|
|
|
|
serializer = TicketMessageCreateRequestSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
v = serializer.validated_data
|
|
|
|
message = TicketMessage.objects.create(ticket=ticket, sender='user', text=v['text'])
|
|
|
|
data = {'messageId': str(message.pk), 'success': True}
|
|
return Response(TicketMessageCreateResponseSerializer(data).data)
|