open the API up to the frontend

سینیور خواست زودتر به فرانت‌اند وصل شویم تا از آنجا هم تست شود. این‌ها
سمتِ ما لازم بودند:

CORS با allowlist صریح (نه ALLOW_ALL): احرازِ هویتِ ما توکنِ Bearer در هدر
است، پس origin ناشناس نباید از مرورگرِ کاربر به API ما بزند.
CORS_ALLOW_CREDENTIALS عمداً خاموش می‌ماند — کوکی‌ای در کار نیست و روشن
کردنش فقط سطحِ حمله اضافه می‌کند. CorsMiddleware بالاتر از CommonMiddleware
گذاشته شد وگرنه ریدایرکتِ APPEND_SLASH و خطای ۴۰۰ هدرِ CORS نمی‌گیرند و در
مرورگر به‌شکلِ خطای مبهمِ شبکه دیده می‌شوند.

PATCH /api/user/profile اضافه شد؛ تا پیش از این هیچ راهی برای ثبتِ نامِ
کاربر نبود. GET /api/home بدونِ پارامتر حالا پایه‌ی ذخیره‌شده‌ی کاربر را
برمی‌گرداند و POST /api/ai/ask فقط با lessonId کار می‌کند — هر دو برای
اینکه فرانت لازم نباشد پایه را خودش نگه دارد. questionCount به
lessons/{id}/intro اضافه شد.

docs/frontend/api.ts و README.md کلاینتِ تایپ‌دار و راهنمای اتصال‌اند، از
روی OpenAPI واقعیِ همین سرویس تولید شده‌اند.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 19:01:06 +03:30
parent fa56308a35
commit ac1b1af398
8 changed files with 697 additions and 7 deletions

111
docs/frontend/README.md Normal file
View File

@@ -0,0 +1,111 @@
# راهنمای اتصالِ فرانت‌اند به این بک‌اند
این پوشه برای تیمِ فرانت (`MathApp_frontend`) است. `api.ts` یک کلاینتِ
تایپ‌دار است که از روی OpenAPI واقعیِ همین سرور نوشته شده — نامِ فیلدها حدس
نیست.
```bash
cp docs/frontend/api.ts <پروژه‌ی-فرانت>/src/lib/api.ts
```
```ts
import { createClient } from './lib/api';
export const api = createClient({ baseUrl: import.meta.env.VITE_API_URL ?? '/api' });
```
## نگاشتِ صفحاتِ فعلیِ فرانت به API
فرانت الان (`src/app/App.tsx`) خروجیِ Figma Make است و هیچ فراخوانیِ APIای
ندارد؛ همه‌ی داده‌ها هاردکد است. این جدول می‌گوید هر صفحه به چه چیزی وصل شود.
| صفحه‌ی فرانت | فراخوانی | نکته |
|---|---|---|
| `SplashScreen` | `api.auth.isSignedIn()` | «قبلاً حساب دارم» → صفحه‌ی ورود |
| — (**وجود ندارد، باید ساخته شود**) | `auth.requestOtp` → `auth.login` | پایین ببینید |
| `OnboardingScreen` | — | ایستا |
| `ProfileSetupScreen` | `profile.update({name})` + `profile.setGrade(id)` | `GRADES` هاردکد را با `api.grades()` عوض کنید |
| `HomeScreen` | `api.home()` + `api.summary()` | بدونِ آرگومان، پایه‌ی خودِ کاربر ملاک است |
| `SkillMapScreen` | `api.home()` → `units[].nodes[]` | `status` = `locked / unlocked / in_progress / completed` |
| `LessonScreen` | `lesson.intro(id)` + `lesson.questions(id)` | پایین ببینید |
| `QuestionScreen` | `lesson.answer(qid, optId)` → `lesson.complete(id)` | `mascotMood` حالتِ رُبو را می‌دهد |
| `AskAIScreen` | `ai.askStream({userQuestion, lessonId}, onDelta)` | فقط `lessonId` کافی‌ست |
| `SubscriptionScreen` | `subscription.plans()` + `subscription.purchase(planId)` | قیمت‌های هاردکد را بردارید |
## چهار چیزی که الان با فرانت جور نیست
**۱. صفحه‌ی ورود وجود ندارد.** فرانت فقط اسم و پایه می‌پرسد، ولی احرازِ هویتِ
ما OTP با شماره‌ی موبایل است. دو راه:
- **مهمان** (سریع‌ترین): `api.auth.guest(deviceId)` — بدونِ شماره، کاربر
بلافاصله وارد می‌شود و بعداً می‌تواند شماره‌اش را وصل کند.
- **کامل**: یک صفحه‌ی «شماره + کد» بین Splash و ProfileSetup.
در حالتِ توسعه (`DJANGO_DEBUG=1`) پاسخِ `requestOtp` فیلدِ `devOtp` دارد، پس
برای تست لازم نیست پیامک واقعی برسد.
**۲. فهرستِ پایه‌ها تا ششم است.** `GRADES` در `App.tsx` هاردکدِ ۱ تا ۶ است، ولی
جهتِ محصول از ششم به بالا رفته (ترانزیشنی: ۶، ۷، ۹، ۱۰). از `api.grades()`
بخوانید تا هر پایه‌ای که اضافه شد خودبه‌خود بیاید.
**۳. صفحه‌ی درس، ویدیو دارد.** `LessonScreen` فهرستی از
`{type: "video" | "quiz"}` نشان می‌دهد. بک‌اند هنوز مفهومِ ویدیو ندارد —
`lessons/{id}/intro` فقط متن و حالتِ ماسکوت می‌دهد. فعلاً فقط `quiz` را نشان
دهید؛ اگر ویدیو واقعاً لازم است بگویید تا در مدل اضافه کنیم.
**۴. «سطح» در آمار.** `HomeScreen` سه عدد نشان می‌دهد: روز پیاپی، امتیاز، سطح.
دوتای اول را داریم (`streak.current`، `xp`)؛ «سطح» را نداریم. یا از روی `xp`
سمتِ فرانت حساب کنید، یا بگویید تا در `/api/user/summary` اضافه کنیم.
## چیزهایی که سمتِ بک‌اند برایتان درست شد
- **CORS** اضافه شد. origin خود را در `CORS_ALLOWED_ORIGINS` بدهید (در حالتِ
توسعه پورت‌های ۳۰۰۰ و ۵۱۷۳ پیش‌فرض بازند).
- **`PATCH /api/user/profile`** اضافه شد — قبلاً پروفایل فقط خواندنی بود و
هیچ راهی برای ثبتِ اسم نبود.
- **`GET /api/home` بدونِ پارامتر** حالا پایه‌ی ذخیره‌شده‌ی کاربر را می‌خواند.
قبلاً اگر `?grade=` نمی‌فرستادید نقشه خالی برمی‌گشت.
- **`POST /api/ai/ask` با فقط `lessonId`** حالا خودش می‌فهمد آن درس مالِ کدام
پایه و فصلِ کتاب است. قبلاً باید `subject` + `grade` + `lessonNumber` را
خودتان می‌فرستادید وگرنه پاسخ بدونِ استناد به کتاب تولید می‌شد.
## چند قاعده که وقت‌تان را می‌خرد
**`Idempotency-Key` بفرستید** روی هر چیزی که پول/امتیاز/قلب جابه‌جا می‌کند
(`answer`, `complete`, `purchase`). یک UUID که تا موفق شدنِ درخواست ثابت
بماند. بدونِ آن، قطعیِ شبکه + دکمه‌ی دوباره = دوبار کم شدنِ قلب.
**۴۰۱ یعنی توکن منقضی شده، نه اینکه کاربر بیرون است.** کلاینت خودش یک‌بار
`refresh` می‌زند و درخواست را تکرار می‌کند؛ فقط اگر آن هم شکست خورد کاربر را
به صفحه‌ی ورود ببرید.
**۴۲۹ و ۵۰۳ را جدا نشان دهید.** ۴۲۹ یعنی سقفِ روزانه‌ی سوال از دستیار پر شده
(الان ۱۵ تا) یا throttle؛ پیامش «فردا برگرد» است. ۵۰۳ یعنی سرویسِ AI موقتاً
قطع است؛ پیامش «چند لحظه دیگر دوباره». `ApiError.isRateLimited` و
`.isProviderDown` همین را می‌دهند.
**پرداخت را در مرورگر باز کنید.** `purchase` یک `paymentUrl` می‌دهد که به
سرورِ خودمان اشاره می‌کند، نه به زرین‌پال. اپ نباید مستقیم با درگاه حرف بزند.
**امتیاز موقعِ اتمامِ درس در مجموع می‌نشیند، نه سرِ هر سوال.** پاسخِ
`answer` فیلدِ `xpEarned` دارد ولی آن ارزشِ همان سوال است؛ `user/summary`
تا وقتی `complete` صدا نخورد تکان نمی‌خورد. در UI می‌توانید خوش‌بینانه
جمع بزنید، ولی عددِ معتبر بعد از `complete` می‌آید.
**`complete` دو فیلدِ اجباری دارد:** `correctCount` و `totalQuestions`.
سرور `correctCount` را با تعدادِ واقعیِ سوال‌های آن درس سقف می‌زند، پس
عددِ متورم امتیاز نمی‌سازد.
**`usedChunks` خالی یعنی پاسخ به کتاب مستند نیست.** اگر خواستید، در UI
تفکیکش کنید («بر اساسِ کتابِ درسی» در برابر «پاسخِ عمومی»).
## اجرای محلی
```bash
# بک‌اند
DJANGO_DEBUG=1 python manage.py runserver # 127.0.0.1:8000
# فرانت
VITE_API_URL=http://127.0.0.1:8000/api npm run dev
```
مستنداتِ زنده‌ی همه‌ی endpointها: `http://127.0.0.1:8000/api/docs/`