- Admin: refund/cancel/mark-resolved/Bazaar-link actions need change permission; question and curriculum import need add/delete; content browser needs view_question; guest device ids are shown to superusers only. - Accounts: registering a guest's phone revokes the guest's earlier sessions; self-deletion keeps Redemption, UserOffer and the phone's OTP rows; the public demo guest cannot be deleted (DEMO_GUEST_DEVICE_IDS); display-name contact filter covers any separator and social handles. - Payment: callback without Authority is 400; a second Bazaar purchase on a paid order is 409; orders snapshot credits/hearts/days at creation (payment.0011). - Assistant: a broken diagram no longer makes a streamed answer free; a late off-topic marker refuses the answer (not stored, not cached); a disconnect saves the partial answer; only KNOWN_COLLECTIONS are searched. - Infra: secret-key file is created atomically (workers no longer split keys); no default INTERNAL_SERVICE_KEY; Kavenegar key and Bazaar token kept out of error logs; Swagger/ReDoc pinned; schema throttled; docker compose bound to 127.0.0.1; non-ASCII digit ids are 400; ticket text capped at 5000. - docs/security-before-production.md: remaining open items 24-31 and the guard tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
5.2 KiB
Python
101 lines
5.2 KiB
Python
"""
|
||
مسیرهای اصلی پروژهی Tadrisino — فاز ۱ (۴ از ۱۱ اپ)
|
||
|
||
هر اپ زیر پیشوند /api/ خودش mount شده. مسیرهای هر endpoint دقیقاً طبق
|
||
سند «Endpointهای API» طراحیشده تا مقایسه با Swagger راحت باشه.
|
||
"""
|
||
import logging
|
||
|
||
from django.contrib import admin
|
||
from django.http import JsonResponse
|
||
from django.urls import include, path
|
||
from django.views import defaults as django_default_views
|
||
from drf_spectacular.utils import extend_schema
|
||
|
||
from config.health import HealthView
|
||
from rest_framework.throttling import ScopedRateThrottle
|
||
from drf_spectacular.views import (
|
||
SpectacularAPIView,
|
||
SpectacularRedocView,
|
||
SpectacularSwaggerView,
|
||
)
|
||
|
||
|
||
@extend_schema(exclude=True)
|
||
class HealthViewSlashAlias(HealthView):
|
||
"""نسخهی اسلشدارِ `/api/health`، فقط برای پایشِ بیرونی — بیرون از اسکیما."""
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def handler404(request, exception=None):
|
||
"""برای مسیرهای /api/ همیشه JSON برمیگردونه؛ بقیه رفتارِ پیشفرضِ جنگو."""
|
||
if request.path.startswith('/api/'):
|
||
return JsonResponse({'detail': 'یافت نشد.'}, status=404)
|
||
return django_default_views.page_not_found(request, exception)
|
||
|
||
|
||
def handler500(request):
|
||
"""شبکهی ایمنیِ نهایی برای exceptionی که حتی به config/exceptions.py هم
|
||
نرسیده (مثلاً در خودِ middleware/URL resolution). traceback کامل رو
|
||
config/middleware.py قبلاً لاگ کرده؛ اینجا فقط یه پاسخِ JSON میده."""
|
||
logger.error('unhandled_500 path=%s', request.path)
|
||
if request.path.startswith('/api/'):
|
||
return JsonResponse(
|
||
{'detail': 'خطای غیرمنتظرهای رخ داد؛ لطفاً بعداً دوباره تلاش کنید.'}, status=500,
|
||
)
|
||
return django_default_views.server_error(request)
|
||
|
||
|
||
class ThrottledSchemaView(SpectacularAPIView):
|
||
"""بیورود و بیکش: هر بار ~۵۶ms CPU و ۱۱۰KB؛ بیthrottle چند ده درخواست در ثانیه workerها را پر میکرد."""
|
||
|
||
throttle_classes = [ScopedRateThrottle]
|
||
throttle_scope = 'read'
|
||
|
||
|
||
urlpatterns = [
|
||
path('admin/', admin.site.urls),
|
||
|
||
# بررسیِ سلامتِ نسخهی مستقرشده — بدونِ احراز، برای پایشِ بیرونی.
|
||
#
|
||
# هر دو شکل ثبت میشود، با و بدونِ اسلشِ آخر. دلیلش تجربهی واقعی است:
|
||
# پایشِ سرور `/api/health/` صدا میزد و چون فقط شکلِ بیاسلش ثبت بود،
|
||
# کسی روی سرور همین فایل را دستی ویرایش کرد تا کار کند — و از آن لحظه
|
||
# هر `git pull` بعدی روی همین فایل گیر میکرد. یک مسیرِ اضافه ارزانش
|
||
# است در برابرِ یک فایلِ دستکاریشده روی پروداکشن.
|
||
path('api/health', HealthView.as_view(), name='health'),
|
||
# همان ویو، فقط برای پایشی که با اسلش صدا میزند. از اسکیما بیرون است تا
|
||
# دو مسیرِ یکسان operationIdهای health_retrieve و health_retrieve_2 نسازند.
|
||
#
|
||
# ⚠️ زیرکلاس لازم است، نه exclude روی خودِ HealthView. extend_schema روی
|
||
# یک کلاس همان کلاس را تغییر میدهد، و چون هر دو مسیر به یک کلاس اشاره
|
||
# میکردند، `/api/health` هم با خودش از اسکیما بیرون میافتاد — یعنی
|
||
# اندپوینتِ سلامت اصلاً در Swagger دیده نمیشد.
|
||
path('api/health/', HealthViewSlashAlias.as_view(), name='health-slash'),
|
||
|
||
# OpenAPI schema + Swagger UI + Redoc
|
||
path('api/schema/', ThrottledSchemaView.as_view(), name='schema'),
|
||
# قالبِ خودمان فقط یک پوستهی راستبهچپ روی قالبِ drf-spectacular است؛
|
||
# بدونش متنِ فارسی در یک بلوکِ LTR رندر میشود و نقطهی جمله و ترتیبِ
|
||
# ستونهای جدول جابهجا میشوند. توضیح در خودِ قالب.
|
||
path('api/docs/', SpectacularSwaggerView.as_view(
|
||
url_name='schema', template_name='tadrisino/swagger_ui.html'), name='swagger-ui'),
|
||
path('api/redoc/', SpectacularRedocView.as_view(
|
||
url_name='schema', template_name='tadrisino/redoc.html'), name='redoc'),
|
||
|
||
# همه زیرِ /api/ جمع میشوند؛ هر اپ مسیرهای خودش را نگه میدارد، پس
|
||
# ترتیبِ این فهرست اهمیتی ندارد و تداخلی بینشان نیست.
|
||
path('api/', include('accounts.urls')),
|
||
path('api/', include('content.urls')),
|
||
path('api/', include('progress.urls')),
|
||
path('api/', include('ai_tutor.urls')),
|
||
path('api/', include('rag.urls')),
|
||
path('api/', include('subscription.urls')),
|
||
path('api/', include('payment.urls')),
|
||
path('api/', include('discount.urls')),
|
||
path('api/', include('notifications.urls')),
|
||
path('api/', include('affiliate.urls')),
|
||
path('api/', include('support.urls')),
|
||
]
|