Files
MathApp/config/urls.py
Zareian a973c41db2 Security review fixes: admin permissions, guest sessions, payment edge cases, assistant leaks
- 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>
2026-10-05 04:52:25 +03:30

101 lines
5.2 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
مسیرهای اصلی پروژه‌ی 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')),
]