make the API docs page answer a consumer's first questions, and fix a missing endpoint
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>
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
| **وضعیت** | روی پروداکشن، در حالِ سرویسدهی |
|
| **وضعیت** | روی پروداکشن، در حالِ سرویسدهی |
|
||||||
| **آدرسِ زنده** | `https://api.math.tadrisino2.ir` · [سلامت](https://api.math.tadrisino2.ir/api/health) |
|
| **آدرسِ زنده** | `https://api.math.tadrisino2.ir` · [سلامت](https://api.math.tadrisino2.ir/api/health) |
|
||||||
| **استک** | Python 3.11+ · Django 5.2 · DRF · SQLite · Qdrant (RAG) |
|
| **استک** | Python 3.11+ · Django 5.2 · DRF · SQLite · Qdrant (RAG) |
|
||||||
| **API** | ۵۵ اندپوینت · مستنداتِ زنده در `/api/docs/` |
|
| **API** | ۵۶ اندپوینت · مستنداتِ زنده در `/api/docs/` |
|
||||||
| **تست** | ۴۸۱ تست، همه سبز (`python manage.py test`) |
|
| **تست** | ۴۸۱ تست، همه سبز (`python manage.py test`) |
|
||||||
| **محتوا** | ۸ پایه · ۵۸ فصل · ۲۶۳ نود · ۲٬۶۱۷ درس · ۲٬۴۶۳ سوال |
|
| **محتوا** | ۸ پایه · ۵۸ فصل · ۲۶۳ نود · ۲٬۶۱۷ درس · ۲٬۴۶۳ سوال |
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class GuestAwareAccountThrottle(ScopedRateThrottle):
|
class GuestAwareAccountThrottle(ScopedRateThrottle):
|
||||||
"""
|
"""
|
||||||
⚠️ اضافه شده: همون الگویِ GuestAwareContentThrottle (content/views.py)،
|
همان الگویِ GuestAwareContentThrottle (content/views.py)،
|
||||||
GuestAwareAIThrottle (ai_tutor/views.py) و بقیه؛ چون GuestSessionView
|
GuestAwareAIThrottle (ai_tutor/views.py) و بقیه؛ چون GuestSessionView
|
||||||
با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید) میسازه، یک
|
با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید) میسازه، یک
|
||||||
ScopedRateThrottleِ سادهٔ per-user توسطِ چرخشِ حساب مهمان دورزدنی
|
ScopedRateThrottleِ سادهٔ per-user توسطِ چرخشِ حساب مهمان دورزدنی
|
||||||
@@ -100,8 +100,11 @@ class GuestSessionView(APIView):
|
|||||||
|
|
||||||
class RequestOTPView(APIView):
|
class RequestOTPView(APIView):
|
||||||
"""
|
"""
|
||||||
POST /api/auth/otp/request — درخواست کد OTP (کمکی، برای تست از طریق Swagger)
|
POST /api/auth/otp/request — درخواستِ کدِ ورود.
|
||||||
در محیط توسعه، کد ساختهشده در پاسخ برگردونده میشه تا نیازی به SMS واقعی نباشه.
|
|
||||||
|
کد فقط با پیامک تحویل داده میشود. دو استثنا: در حالتِ DEBUG کد در فیلدِ
|
||||||
|
devOtp برمیگردد تا تستِ محلی به پیامکِ واقعی نیاز نداشته باشد، و برای
|
||||||
|
شمارههای TEST_LOGIN_PHONES کدِ ثابتِ از پیش تعریفشده معتبر است.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
throttle_scope = 'otp_request'
|
throttle_scope = 'otp_request'
|
||||||
@@ -117,13 +120,13 @@ class RequestOTPView(APIView):
|
|||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
phone = serializer.validated_data['phone']
|
phone = serializer.validated_data['phone']
|
||||||
|
|
||||||
# شمارهی تستِ تیم (TEST_LOGIN_PHONES): کدش ثابت و از قبل معلومه، پس نه
|
# شمارهی تستِ تیم: کد ثابت و از قبل معلوم است، پس نه ردیفِ OTP تازهای
|
||||||
# ردیفِ OTP تازهای لازمه نه پیامکی. cooldown و سقفِ ساعتی هم رد میشن —
|
# لازم است نه پیامکی. cooldown و سقفِ ساعتی هم رد میشوند چون هر دو از
|
||||||
# اونها برای محافظت از گیرندهی واقعیِ پیامکاند و اینجا پیامکی در کار
|
# گیرندهی واقعیِ پیامک محافظت میکنند و اینجا پیامکی در کار نیست.
|
||||||
# نیست؛ نگهداشتنشون فقط توسعهدهنده رو ۲ دقیقه معطل میکرد.
|
#
|
||||||
# ⚠️ کد عمداً در پاسخ برنمیگرده (برخلافِ devOtp در حالتِ DEBUG): این
|
# ⚠️ کد عمداً در پاسخ برنمیگردد، برخلافِ devOtp در حالتِ DEBUG: این
|
||||||
# مسیر روی سرورِ عمومی هم فعاله، و برگردوندنش یعنی هر کسی که شماره رو
|
# مسیر روی سرورِ عمومی هم فعال است، پس برگرداندنش یعنی هر کسی که شماره
|
||||||
# بداند میتواند وارد شود. سقفِ تلاشِ ورود در LoginView سرِ جاشه.
|
# را بداند میتواند وارد شود.
|
||||||
if phone in settings.TEST_LOGIN_PHONES:
|
if phone in settings.TEST_LOGIN_PHONES:
|
||||||
logger.info('OTP request for configured test phone: phone_suffix=%s', phone[-4:])
|
logger.info('OTP request for configured test phone: phone_suffix=%s', phone[-4:])
|
||||||
data = {
|
data = {
|
||||||
@@ -377,10 +380,9 @@ class SetUserGradeView(APIView):
|
|||||||
"""POST /api/user/grade — ثبت پایهی کاربر (نیاز به احراز هویت)"""
|
"""POST /api/user/grade — ثبت پایهی کاربر (نیاز به احراز هویت)"""
|
||||||
|
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
# ⚠️ اضافه شده: این endpoint قبلاً هیچ throttleای نداشت؛ یک کاربرِ
|
# بدونِ throttle، یک کاربرِ احرازهویتشده (از جمله مهمان) میتواند
|
||||||
# (از جمله مهمان) احرازهویتشده میتونست تعدادِ نامحدودی درخواستِ
|
# نامحدود درخواستِ نوشتن روی دیتابیس بفرستد. کلیدِ شمارش برای مهمانها
|
||||||
# نوشتنِ دیتابیس بفرسته. طبقِ همون الگویِ سایرِ اپها، یک
|
# باید IP باشد نه pk — دلیلش در GuestAwareAccountThrottle بالا.
|
||||||
# throttle_scope مجزا با GuestAwareAccountThrottle اضافه شد.
|
|
||||||
throttle_scope = 'user_profile_write'
|
throttle_scope = 'user_profile_write'
|
||||||
throttle_classes = [GuestAwareAccountThrottle]
|
throttle_classes = [GuestAwareAccountThrottle]
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from .serializers import (
|
|||||||
|
|
||||||
class GuestAwareAffiliateThrottle(ScopedRateThrottle):
|
class GuestAwareAffiliateThrottle(ScopedRateThrottle):
|
||||||
"""
|
"""
|
||||||
⚠️ اضافه شده: همون الگویِ GuestAwareDiscountThrottle/GuestAwareFamilyThrottle
|
همان الگویِ GuestAwareDiscountThrottle/GuestAwareFamilyThrottle
|
||||||
و بقیه؛ چرخشِ حسابِ مهمان (deviceId جدید → pk جدید) یک ScopedRateThrottleِ
|
و بقیه؛ چرخشِ حسابِ مهمان (deviceId جدید → pk جدید) یک ScopedRateThrottleِ
|
||||||
سادهٔ per-user رو دور میزد. اینجا حساستره چون referralCode یک
|
سادهٔ per-user رو دور میزد. اینجا حساستره چون referralCode یک
|
||||||
شناسهی حدسزدنیست (404 برای کدِ نامعتبر = oracle) و یک حدسِ درست
|
شناسهی حدسزدنیست (404 برای کدِ نامعتبر = oracle) و یک حدسِ درست
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def _annotation(retrieval_meta: dict, generation_meta: dict, *, from_cache: bool
|
|||||||
|
|
||||||
class GuestAwareAIThrottle(ScopedRateThrottle):
|
class GuestAwareAIThrottle(ScopedRateThrottle):
|
||||||
"""
|
"""
|
||||||
⚠️ اضافه شده: ScopedRateThrottle استاندارد برای کاربرِ احرازهویتشده
|
ScopedRateThrottle استاندارد برای کاربرِ احرازهویتشده
|
||||||
(مهمان یا نه) از request.user.pk بهعنوانِ کلیدِ throttle استفاده
|
(مهمان یا نه) از request.user.pk بهعنوانِ کلیدِ throttle استفاده
|
||||||
میکنه. چون GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ
|
میکنه. چون GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ
|
||||||
تازه (pk جدید) میسازه، یک مهاجم میتونست با ساختنِ پیدرپیِ حسابهای
|
تازه (pk جدید) میسازه، یک مهاجم میتونست با ساختنِ پیدرپیِ حسابهای
|
||||||
|
|||||||
@@ -180,30 +180,28 @@ INSTALLED_APPS = [
|
|||||||
'drf_spectacular',
|
'drf_spectacular',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
|
|
||||||
# Tadrisino apps (فاز اول - ۴ از ۱۱ اپ)
|
# اپهای خودمان. هر کدام یک مسئولیت و مدلهای خودش را دارد؛ ارجاعِ بینِ
|
||||||
|
# اپها با user_id/object_id انجام میشود نه ForeignKey، تا هر کدام بعداً
|
||||||
|
# قابلِ جدا شدن بماند (docs/architecture.md).
|
||||||
|
|
||||||
|
# مسیرِ اصلیِ کاربر: ورود، محتوا، پیشرفت، دستیار
|
||||||
'accounts',
|
'accounts',
|
||||||
'content',
|
'content',
|
||||||
'progress',
|
'progress',
|
||||||
'ai_tutor',
|
'ai_tutor',
|
||||||
|
|
||||||
# Tadrisino apps (فاز دوم - بخش ۱ از ۳: subscription, payment, discount)
|
# کسبِ درآمد
|
||||||
'subscription',
|
'subscription',
|
||||||
'payment',
|
'payment',
|
||||||
'discount',
|
'discount',
|
||||||
|
|
||||||
# Tadrisino apps (فاز دوم - بخش ۲: notifications)
|
# ارتباط با کاربر و پشتیبانی
|
||||||
'notifications',
|
'notifications',
|
||||||
|
|
||||||
# Tadrisino apps (فاز دوم - بخش ۳: affiliate)
|
|
||||||
'affiliate',
|
'affiliate',
|
||||||
|
|
||||||
# Tadrisino apps (فاز دوم - بخش ۴: support)
|
|
||||||
'support',
|
'support',
|
||||||
|
|
||||||
# Tadrisino apps (فاز دوم - بخش ۵: family)
|
|
||||||
'family',
|
'family',
|
||||||
|
|
||||||
# Tadrisino apps (سیستم پرداخت بازار + آفر/تخفیف + لاگ عمومی تغییر Status)
|
# لاگِ عمومیِ تغییرِ وضعیت، مستقل از بقیهی اپها
|
||||||
'statuslog',
|
'statuslog',
|
||||||
|
|
||||||
# Tadrisino apps (مدلِ مینیمالِ ذخیرهسازیِ کمپینهای پیامی، بدونِ منطقِ ارسال)
|
# Tadrisino apps (مدلِ مینیمالِ ذخیرهسازیِ کمپینهای پیامی، بدونِ منطقِ ارسال)
|
||||||
@@ -429,12 +427,100 @@ REST_FRAMEWORK = {
|
|||||||
|
|
||||||
SPECTACULAR_SETTINGS = {
|
SPECTACULAR_SETTINGS = {
|
||||||
'TITLE': 'Tadrisino API',
|
'TITLE': 'Tadrisino API',
|
||||||
|
# این متن قراردادِ بینِ ما و تیمِ فرانت است و تنها جاییست که هر دو طرف
|
||||||
|
# میبینند. پس بهجای فهرستِ امکانات، به سه سوالِ اولِ هر مصرفکننده جواب
|
||||||
|
# میدهد: چطور توکن بگیرم، خطا چه شکلی برمیگردد، و کِی محدود میشوم.
|
||||||
|
# Swagger UI و Redoc هر دو Markdown را رندر میکنند.
|
||||||
'DESCRIPTION': (
|
'DESCRIPTION': (
|
||||||
'بکاند اپلیکیشن آموزشی Tadrisino — احراز هویت با OTP، محتوای درسی '
|
'بکاندِ اپلیکیشنِ آموزشیِ تدریسینو: برنامهی درسی، آزمون، پیشرفتِ '
|
||||||
'و پیشرفت کاربر، دستیار هوش مصنوعی، اشتراک و پرداخت، تخفیف، '
|
'کاربر، دستیارِ هوشِ مصنوعی، اشتراک و پرداخت.\n'
|
||||||
'اعلانها، همکاری در فروش، پشتیبانی و حساب خانواده.'
|
'\n'
|
||||||
|
'## احرازِ هویت\n'
|
||||||
|
'\n'
|
||||||
|
'همهی درخواستهای احرازشده با یک توکنِ Bearer انجام میشوند:\n'
|
||||||
|
'\n'
|
||||||
|
'```\n'
|
||||||
|
'Authorization: Bearer <token>\n'
|
||||||
|
'```\n'
|
||||||
|
'\n'
|
||||||
|
'دو راه برای گرفتنِ توکن هست:\n'
|
||||||
|
'\n'
|
||||||
|
'| مسیر | کاربرد |\n'
|
||||||
|
'|---|---|\n'
|
||||||
|
'| `POST /api/auth/session` | ورودِ مهمان با `deviceId` — بدونِ شماره، بلافاصله |\n'
|
||||||
|
'| `POST /api/auth/otp/request` + `POST /api/auth/login` | ورودِ کامل با شمارهی موبایل |\n'
|
||||||
|
'\n'
|
||||||
|
'کاربرِ مهمان میتواند بعداً شمارهاش را وصل کند و همان حساب را نگه '
|
||||||
|
'دارد. توکنِ دسترسی یک ساعت اعتبار دارد؛ با `POST /api/auth/refresh` '
|
||||||
|
'تازهاش کنید.\n'
|
||||||
|
'\n'
|
||||||
|
'**در همین صفحه:** روی **Authorize** بزنید و *فقط خودِ توکن* را وارد '
|
||||||
|
'کنید — کلمهی `Bearer` را Swagger خودش اضافه میکند.\n'
|
||||||
|
'\n'
|
||||||
|
'اندپوینتی که توکن نگیرد، درخواست را ناشناس میبیند؛ اینکه ورود لازم '
|
||||||
|
'باشد یا نه، به خودِ آن اندپوینت بستگی دارد.\n'
|
||||||
|
'\n'
|
||||||
|
'## خطاها\n'
|
||||||
|
'\n'
|
||||||
|
'هر خطا — حتی خطای پیشبینینشده — یک پاسخِ JSON است، نه صفحهی HTML. '
|
||||||
|
'پیامِ `detail` فارسی و قابلِ نمایش به کاربرِ نهایی است.\n'
|
||||||
|
'\n'
|
||||||
|
'```json\n'
|
||||||
|
'{"detail": "کد OTP نامعتبر یا منقضیشده است."}\n'
|
||||||
|
'```\n'
|
||||||
|
'\n'
|
||||||
|
'| کد | معنی |\n'
|
||||||
|
'|---|---|\n'
|
||||||
|
'| `400` | ورودی نامعتبر است |\n'
|
||||||
|
'| `401` | توکن نیست، نامعتبر است یا منقضی شده |\n'
|
||||||
|
'| `403` | توکن معتبر است ولی اجازهی این کار را ندارد |\n'
|
||||||
|
'| `404` | چیزی با این شناسه وجود ندارد |\n'
|
||||||
|
'| `429` | از محدودیتِ نرخ گذشتید — بخشِ بعد |\n'
|
||||||
|
'| `503` | سرویسِ بیرونی (پیامک، پرداخت) در دسترس نیست |\n'
|
||||||
|
'\n'
|
||||||
|
'`503` عمدیست: وقتی سرویسِ بیرونی تنظیم یا در دسترس نباشد، پاسخِ '
|
||||||
|
'صریح میگیرید، نه موفقیتِ ساختگی.\n'
|
||||||
|
'\n'
|
||||||
|
'## محدودیتِ نرخ\n'
|
||||||
|
'\n'
|
||||||
|
'اندپوینتهای حساس (ورود، درخواستِ کد، پرسش از دستیار) سقفِ مستقل '
|
||||||
|
'دارند. عبور از سقف یعنی `429`، و هر جا که معنی داشته باشد هدرِ '
|
||||||
|
'`Retry-After` هم میآید. کاربرِ مهمان بر اساسِ IP شمرده میشود.\n'
|
||||||
|
'\n'
|
||||||
|
'مسیرِ ورود سقفِ جداگانهای بر اساسِ *شماره* هم دارد، مستقل از IP، تا '
|
||||||
|
'پخش کردنِ تلاش بینِ چند IP فایدهای نداشته باشد.\n'
|
||||||
|
'\n'
|
||||||
|
'## قراردادها\n'
|
||||||
|
'\n'
|
||||||
|
'- نامِ فیلدها `camelCase` است (`deviceId`, `refreshToken`, `nodeId`).\n'
|
||||||
|
'- زمانها ISO 8601 با منطقهی زمانیاند.\n'
|
||||||
|
'- متنِ رو به کاربر فارسی است؛ شناسهها و نامِ فیلدها انگلیسی.\n'
|
||||||
|
'- این اسکیما **مرجعِ قرارداد** است. کلاینت را از روی حافظه ننویسید — '
|
||||||
|
'از `/api/schema/` تولیدش کنید.\n'
|
||||||
),
|
),
|
||||||
'VERSION': '1.0.0',
|
'VERSION': '1.0.0',
|
||||||
|
'CONTACT': {'name': 'تیمِ بکاندِ تدریسینو'},
|
||||||
|
'EXTERNAL_DOCS': {
|
||||||
|
'description': 'راهنمای اتصالِ فرانتاند',
|
||||||
|
'url': 'https://gitea.tadrisino2.ir/Tadrisino/MathApp/src/branch/main/docs/frontend/README.md',
|
||||||
|
},
|
||||||
|
# ترتیبِ تگها در Swagger به همین ترتیب است، نه الفبایی — یعنی مسیرِ
|
||||||
|
# واقعیِ یک کاربر: اول وارد شو، بعد محتوا بگیر، بعد پیشرفت و بقیه.
|
||||||
|
'TAGS': [
|
||||||
|
{'name': 'Identity & Profile (accounts)', 'description': 'ورود، session، پروفایل'},
|
||||||
|
{'name': 'Curriculum (content)', 'description': 'پایه، فصل، نود، درس، سوال، آزمون'},
|
||||||
|
{'name': 'Progress & Gamification (progress)', 'description': 'آمار، streak، لیدربرد، شخصیتها'},
|
||||||
|
{'name': 'AI Tutor (ai_tutor)', 'description': 'پرسش از دستیار و بازخورد روی پاسخ'},
|
||||||
|
{'name': 'RAG (rag)', 'description': 'بازیابی از محتوای کتاب — داخلی'},
|
||||||
|
{'name': 'Subscription (subscription)', 'description': 'پلنها و اشتراکِ کاربر'},
|
||||||
|
{'name': 'Payment (payment)', 'description': 'سفارش و تراکنش (زرینپال و کافهبازار)'},
|
||||||
|
{'name': 'Discount (discount)', 'description': 'کدِ تخفیف و آفرها'},
|
||||||
|
{'name': 'Notifications (notifications)', 'description': 'اعلانهای کاربر'},
|
||||||
|
{'name': 'Family (family)', 'description': 'کنترلِ والدین و گزارشِ هفتگی'},
|
||||||
|
{'name': 'Support (support)', 'description': 'تیکت و گزارشِ اشکال'},
|
||||||
|
{'name': 'Affiliate (affiliate)', 'description': 'برنامهی معرفی و پورسانت'},
|
||||||
|
{'name': 'System', 'description': 'سلامتِ سرویس'},
|
||||||
|
],
|
||||||
'SERVE_INCLUDE_SCHEMA': False,
|
'SERVE_INCLUDE_SCHEMA': False,
|
||||||
'COMPONENT_SPLIT_REQUEST': True,
|
'COMPONENT_SPLIT_REQUEST': True,
|
||||||
'SWAGGER_UI_SETTINGS': {
|
'SWAGGER_UI_SETTINGS': {
|
||||||
@@ -483,35 +569,14 @@ OTP_REQUEST_PHONE_COOLDOWN_SECONDS = 120
|
|||||||
# فقط وقتی DEBUG=True مقدار دارد؛ در پروداکشن None است.
|
# فقط وقتی DEBUG=True مقدار دارد؛ در پروداکشن None است.
|
||||||
DEV_MASTER_OTP = '0000' if DEBUG else None
|
DEV_MASTER_OTP = '0000' if DEBUG else None
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# شمارههای تستِ ورود: «شماره:کد» با کامای جداکننده. برای این شمارهها پیامک
|
||||||
# شمارههای تستِ ورود (TEST_LOGIN_PHONES)
|
# ارسال نمیشود و همان کدِ ثابت معتبر است — تا ورودِ تیمِ توسعه به تنظیم بودنِ
|
||||||
# -----------------------------------------------------------------------
|
# کاوهنگار یا به یک آدمِ خاص وابسته نباشد. برخلافِ DEV_MASTER_OTP «هر
|
||||||
# مشکلی که این حل میکند: با DEBUG=0 و بدونِ کاوهنگار، هیچ توسعهدهندهای
|
# شمارهای» نیست و به DEBUG گره نخورده. روالِ استفاده در docs/operations.md.
|
||||||
# نمیتواند وارد API شود مگر اینکه کسی با دسترسیِ ادمین دستی یک ردیفِ OTPCode
|
|
||||||
# بسازد. یعنی ورودِ هر عضوِ تیم به یک آدمِ خاص وابسته بود.
|
|
||||||
#
|
#
|
||||||
# این همان الگویی است که اپهای واقعی برای همین کار دارند (Firebase Auth
|
# ⚠️ شماره باید *دقیقاً* همان رشتهای باشد که کلاینت میفرستد: این کدبیس شماره
|
||||||
# «fictional phone numbers»، Apple «demo account» برای بررسیِ App Store):
|
|
||||||
# چند شمارهی مشخص با کدِ ثابت، که پیامک برایشان ارسال نمیشود. برخلافِ
|
|
||||||
# DEV_MASTER_OTP این «هر شمارهای» نیست — فقط همین چند شماره، و هر کدام
|
|
||||||
# کدِ خودش را دارد.
|
|
||||||
#
|
|
||||||
# TEST_LOGIN_PHONES=09120000001:834512,09120000002:110947
|
|
||||||
#
|
|
||||||
# قواعدی که عمداً رعایت میشوند:
|
|
||||||
# • کد هیچوقت در پاسخِ API برنمیگردد — هر کس شماره را بداند هم نمیتواند
|
|
||||||
# کد را از سرور بیرون بکشد.
|
|
||||||
# • سقفِ تلاشِ ورودِ هر شماره (OTP_LOGIN_PHONE_MAX_ATTEMPTS) روی اینها هم
|
|
||||||
# اعمال میشود، پس حدسزدنِ کد با brute-force بسته است.
|
|
||||||
# • ورودیِ خراب باعثِ بالا نیامدنِ سرویس میشود، نه نادیده گرفته شدنِ بیصدا.
|
|
||||||
# • /api/health وجودشان را هشدار میدهد تا روی سرور فراموش نشوند.
|
|
||||||
#
|
|
||||||
# ⚠️ شماره باید *دقیقاً* همان رشتهای باشد که کلاینت میفرستد. این کدبیس شماره
|
|
||||||
# را normalize نمیکند (accounts/serializers.py فقط CharField است)، پس
|
# را normalize نمیکند (accounts/serializers.py فقط CharField است)، پس
|
||||||
# «09121111111» و «+989121111111» دو کاربرِ جدا هستند و تطبیق رشتهای است.
|
# «09121111111» و «+989121111111» دو کاربرِ جدا هستند.
|
||||||
#
|
|
||||||
# ⚠️ این حسابها حسابِ تستاند: کدِ واقعی و تصادفی بگذارید (نه 1234)، و هیچ
|
|
||||||
# دادهی واقعیِ کاربری رویشان نگذارید.
|
|
||||||
def _parse_test_login_phones(raw: str) -> dict[str, str]:
|
def _parse_test_login_phones(raw: str) -> dict[str, str]:
|
||||||
pairs = {}
|
pairs = {}
|
||||||
for item in raw.split(','):
|
for item in raw.split(','):
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ from drf_spectacular.views import (
|
|||||||
SpectacularSwaggerView,
|
SpectacularSwaggerView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(exclude=True)
|
||||||
|
class HealthViewSlashAlias(HealthView):
|
||||||
|
"""نسخهی اسلشدارِ `/api/health`، فقط برای پایشِ بیرونی — بیرون از اسکیما."""
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -52,37 +57,32 @@ urlpatterns = [
|
|||||||
# هر `git pull` بعدی روی همین فایل گیر میکرد. یک مسیرِ اضافه ارزانش
|
# هر `git pull` بعدی روی همین فایل گیر میکرد. یک مسیرِ اضافه ارزانش
|
||||||
# است در برابرِ یک فایلِ دستکاریشده روی پروداکشن.
|
# است در برابرِ یک فایلِ دستکاریشده روی پروداکشن.
|
||||||
path('api/health', HealthView.as_view(), name='health'),
|
path('api/health', HealthView.as_view(), name='health'),
|
||||||
# همان ویو، فقط برای پایشی که با اسلش صدا میزند. از اسکیما بیرون است تا دو
|
# همان ویو، فقط برای پایشی که با اسلش صدا میزند. از اسکیما بیرون است تا
|
||||||
# مسیرِ یکسان operationIdهای health_retrieve و health_retrieve_2 نسازند.
|
# دو مسیرِ یکسان operationIdهای health_retrieve و health_retrieve_2 نسازند.
|
||||||
path('api/health/', extend_schema(exclude=True)(HealthView).as_view(),
|
#
|
||||||
name='health-slash'),
|
# ⚠️ زیرکلاس لازم است، نه exclude روی خودِ HealthView. extend_schema روی
|
||||||
|
# یک کلاس همان کلاس را تغییر میدهد، و چون هر دو مسیر به یک کلاس اشاره
|
||||||
|
# میکردند، `/api/health` هم با خودش از اسکیما بیرون میافتاد — یعنی
|
||||||
|
# اندپوینتِ سلامت اصلاً در Swagger دیده نمیشد.
|
||||||
|
path('api/health/', HealthViewSlashAlias.as_view(), name='health-slash'),
|
||||||
|
|
||||||
# OpenAPI schema + Swagger UI + Redoc
|
# OpenAPI schema + Swagger UI + Redoc
|
||||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||||
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||||
|
|
||||||
# اپهای فاز ۱
|
# همه زیرِ /api/ جمع میشوند؛ هر اپ مسیرهای خودش را نگه میدارد، پس
|
||||||
|
# ترتیبِ این فهرست اهمیتی ندارد و تداخلی بینشان نیست.
|
||||||
path('api/', include('accounts.urls')),
|
path('api/', include('accounts.urls')),
|
||||||
path('api/', include('content.urls')),
|
path('api/', include('content.urls')),
|
||||||
path('api/', include('progress.urls')),
|
path('api/', include('progress.urls')),
|
||||||
path('api/', include('ai_tutor.urls')),
|
path('api/', include('ai_tutor.urls')),
|
||||||
path('api/', include('rag.urls')),
|
path('api/', include('rag.urls')),
|
||||||
|
|
||||||
# اپهای فاز ۲ / بخش ۱ (subscription, payment, discount)
|
|
||||||
path('api/', include('subscription.urls')),
|
path('api/', include('subscription.urls')),
|
||||||
path('api/', include('payment.urls')),
|
path('api/', include('payment.urls')),
|
||||||
path('api/', include('discount.urls')),
|
path('api/', include('discount.urls')),
|
||||||
|
|
||||||
# اپهای فاز ۲ / بخش ۲ (notifications)
|
|
||||||
path('api/', include('notifications.urls')),
|
path('api/', include('notifications.urls')),
|
||||||
|
|
||||||
# اپهای فاز ۲ / بخش ۳ (affiliate)
|
|
||||||
path('api/', include('affiliate.urls')),
|
path('api/', include('affiliate.urls')),
|
||||||
|
|
||||||
# اپهای فاز ۲ / بخش ۴ (support)
|
|
||||||
path('api/', include('support.urls')),
|
path('api/', include('support.urls')),
|
||||||
|
|
||||||
# اپهای فاز ۲ / بخش ۵ (family)
|
|
||||||
path('api/', include('family.urls')),
|
path('api/', include('family.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -626,7 +626,7 @@ class QuestionReportView(APIView):
|
|||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=QuestionReportRequestSerializer,
|
request=QuestionReportRequestSerializer,
|
||||||
responses=QuestionReportResponseSerializer,
|
responses=QuestionReportResponseSerializer,
|
||||||
tags=['محتوا (content)'],
|
tags=['Curriculum (content)'],
|
||||||
)
|
)
|
||||||
def post(self, request, question_id: int):
|
def post(self, request, question_id: int):
|
||||||
question = Question.objects.filter(pk=question_id).first()
|
question = Question.objects.filter(pk=question_id).first()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from .services import redeem_offer_code
|
|||||||
|
|
||||||
class GuestAwareDiscountThrottle(ScopedRateThrottle):
|
class GuestAwareDiscountThrottle(ScopedRateThrottle):
|
||||||
"""
|
"""
|
||||||
⚠️ اضافه شده: همون مشکلِ GuestAwareAIThrottle (ai_tutor/views.py) و
|
همان مشکلِ GuestAwareAIThrottle (ai_tutor/views.py) و
|
||||||
GuestAwareFamilyThrottle (family/views.py) اینجا هم هست.
|
GuestAwareFamilyThrottle (family/views.py) اینجا هم هست.
|
||||||
ScopedRateThrottle استاندارد برای درخواستِ احرازهویتشده از
|
ScopedRateThrottle استاندارد برای درخواستِ احرازهویتشده از
|
||||||
request.user.pk بهعنوانِ کلیدِ throttle استفاده میکنه؛ چون
|
request.user.pk بهعنوانِ کلیدِ throttle استفاده میکنه؛ چون
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def _week_start(dt):
|
|||||||
|
|
||||||
class GuestAwareFamilyThrottle(ScopedRateThrottle):
|
class GuestAwareFamilyThrottle(ScopedRateThrottle):
|
||||||
"""
|
"""
|
||||||
⚠️ اضافه شده: همون مشکلِ GuestAwareAIThrottle (ai_tutor/views.py) اینجا
|
همان مشکلِ GuestAwareAIThrottle (ai_tutor/views.py) اینجا
|
||||||
هم هست. ScopedRateThrottle استاندارد برای درخواستِ احرازهویتشده از
|
هم هست. ScopedRateThrottle استاندارد برای درخواستِ احرازهویتشده از
|
||||||
request.user.pk بهعنوانِ کلیدِ throttle استفاده میکنه؛ چون
|
request.user.pk بهعنوانِ کلیدِ throttle استفاده میکنه؛ چون
|
||||||
GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید)
|
GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from .serializers import (
|
|||||||
|
|
||||||
class GuestAwareSupportThrottle(ScopedRateThrottle):
|
class GuestAwareSupportThrottle(ScopedRateThrottle):
|
||||||
"""
|
"""
|
||||||
⚠️ اضافه شده: همون مشکلِ GuestAwareAIThrottle (ai_tutor/views.py)،
|
همان مشکلِ GuestAwareAIThrottle (ai_tutor/views.py)،
|
||||||
GuestAwareFamilyThrottle (family/views.py) و GuestAwareDiscountThrottle/
|
GuestAwareFamilyThrottle (family/views.py) و GuestAwareDiscountThrottle/
|
||||||
GuestAwarePurchaseThrottle (discount/views.py، subscription/views.py)
|
GuestAwarePurchaseThrottle (discount/views.py، subscription/views.py)
|
||||||
اینجا هم هست. ScopedRateThrottle استاندارد برای درخواستِ
|
اینجا هم هست. ScopedRateThrottle استاندارد برای درخواستِ
|
||||||
|
|||||||
Reference in New Issue
Block a user