diff --git a/.claude/prompt/adapt-backend-audit-api.md b/.claude/prompt/adapt-backend-audit-api.md new file mode 100644 index 0000000..78c6c04 --- /dev/null +++ b/.claude/prompt/adapt-backend-audit-api.md @@ -0,0 +1,118 @@ +# تطبیق سایت عمومی با تغییرات API برنچ `backend-audit` + +> ✅ **حل‌شده (Path A) — هیچ تغییری در `nobat724_front` لازم نیست.** +> مشکل rotation با **نرم‌کردن backend** حل شد: در `clinicpro` (commit روی `backend-audit`) چرخش/باطل‌سازی single-use از `/oauth/token/refresh` برداشته شد و توکن refresh دوباره قابل‌استفاده شد (فقط چک کاربر معلق `status!=1` باقی ماند). پس `lib/serverToken.js` بدون تغییر کار می‌کند. +> تنها نکته‌ی باقی‌مانده **اختیاری** است: ۵۰-cap نظرات (وظیفه ۲ پایین) — اگر ۵۰ نظر کافی است، کاری لازم نیست. + +## پروژه + +`nobat724_front` (سایت عمومی). پرامپت همتا برای Admin SPA: `clinicpro/.claude/prompt/admin-spa-adapt-backend-audit.md`. منبع تغییرات: برنچ `backend-audit` در `clinicpro` (۳۳ commit). + +## زمینه + +ممیزی بک‌اند چند endpoint را تغییر داد. مقدارهای wire کدهای خطا حفظ شده‌اند، ولی **یک تغییر رفتارِ شکننده** برای سایت عمومی وجود دارد (rotation توکن refresh) و یک تغییر کم‌اهمیت (صفحه‌بندی نظرات). این پرامپت مصرف‌کننده‌های `nobat724_front` را اصلاح می‌کند. + +## جدول تغییرات API مرتبط با سایت عمومی + +| Endpoint | تغییر | مصرف در nobat724 | شدت | +|---|---|---|---| +| `POST /oauth/token/refresh` | refresh_token اکنون **یک‌بارمصرف** است و در هر فراخوانی **چرخش** می‌کند؛ کاربر معلق `401` | `lib/serverToken.js` | 🔴 **شکننده** | +| `GET /api/v1/comments/{uuid}` | `data.meta` افزوده شد، پیش‌فرض **۵۰ نظر** (قبلاً همه) | `app/doctor/[slug]/page.js`, `services/response.js` `getDoctorComments` | 🟡 کم | +| `GET /api/v1/insurance/{id}`, `GET /clinic-pro/doctor-address/{id}`, `appointment-settings/{...}` | حالا owner/admin (`403`) | مصرف نمی‌شود | ⚪ بدون اثر | +| کدهای خطای legacy (M21) | مقدار wire بدون تغییر | — | ⚪ بدون اثر | + +## 🔴 مشکل اصلی: rotation توکن refresh (`POST /oauth/token/refresh`) + +### رفتار جدید backend +هر فراخوانی `/oauth/token/refresh`: +1. توکن ارائه‌شده را **باطل** می‌کند (single-use)، +2. یک جفت `access_token` + **`refresh_token` جدید** صادر می‌کند، +3. کاربر با `status != 1` → `401`. + +پاسخ: +```json +{ "access_token": "…", "refresh_token": "<توکن جدید — با قبلی فرق دارد>", "token_type": "Bearer", "expires_in": 900, "refresh_token_expires_in": 2592000 } +``` + +### وضعیت فعلی nobat724 (کد واقعی — `lib/serverToken.js`) + +```js +export async function getServerAccessToken() { + const cookieStore = await cookies(); + const refreshToken = cookieStore.get("refresh_token")?.value; + if (!refreshToken) return null; + try { + const res = await axiosInstance.post( + `${process.env.NEXT_PUBLIC_API_URL}/oauth/token/refresh`, + { refresh_token: refreshToken }, + { headers: { "Content-Type": "application/json", Authorization: "" } } + ); + return res.data?.access_token ?? null; // ⚠️ فقط access_token خوانده می‌شود + } catch { return null; } +} +``` + +### چرا می‌شکند +- این تابع **در هر بار رندر صفحه** صدا زده می‌شود (مثلاً `app/dashboard/page.js`). +- `refresh_token` چرخش‌یافته‌ی جدید را **ذخیره نمی‌کند** و توکن قدیمیِ کوکی پس از اولین refresh **باطل** شده است. +- نتیجه: refresh اول OK → ناوبری بعدی همان کوکی باطل را می‌فرستد → `401` → `null` → **redirect به login** (خروج عملی کاربر). +- **محدودیت Next.js:** `getServerAccessToken()` از یک **Server Component** صدا زده می‌شود؛ Server Componentها **نمی‌توانند کوکی ست کنند** (فقط Server Action / Route Handler / middleware می‌توانند). پس ذخیره‌ی توکن چرخش‌یافته در همین تابع ممکن نیست. + +### وظیفه ۱ — حل rotation (یکی از دو مسیر؛ مسیر A توصیه می‌شود) + +> این یک **تصمیم cross-repo** است. قبل از پیاده‌سازی، با تیم بک‌اند هماهنگ کن. + +**مسیر A (توصیه‌شده — تغییر در backend):** +چون سایت در هر رندر refresh می‌زند، single-use rotation با این معماری ناسازگار است. بهترین کار: در `clinicpro` `AuthController::refreshToken` **بخشِ چرخش/باطل‌سازی را بردار** و فقط **چک status** (کاربر معلق `401`) را نگه‌دار. در این حالت `serverToken.js` نیازی به تغییر ندارد. (یک پرامپت backend جدا برای این کار بساز.) + +**مسیر B (تغییر در nobat724 — اگر rotation باید بماند):** +refresh را به جایی منتقل کن که **اجازه‌ی ست‌کردن کوکی دارد**: +- یک **Route Handler** (`app/api/refresh/route.js`) یا **middleware** که `/oauth/token/refresh` را صدا بزند و **`res.data.refresh_token` و `access_token` جدید را در کوکی بنویسد**، سپس صفحات به‌جای فراخوانی مستقیم، از این مسیر استفاده کنند. +- نمونه (Route Handler): +```js +// app/api/refresh/route.js +import { cookies } from "next/headers"; +export async function POST() { + const jar = await cookies(); + const rt = jar.get("refresh_token")?.value; + if (!rt) return Response.json({ ok: false }, { status: 401 }); + const r = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/oauth/token/refresh`, { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refresh_token: rt }), + }); + if (!r.ok) return Response.json({ ok: false }, { status: 401 }); + const d = await r.json(); + jar.set("access_token", d.access_token, { httpOnly: true, secure: true, sameSite: "lax", maxAge: d.expires_in }); + jar.set("refresh_token", d.refresh_token, { httpOnly: true, secure: true, sameSite: "lax", maxAge: d.refresh_token_expires_in }); // ← توکن چرخش‌یافته + return Response.json({ ok: true, access_token: d.access_token }); +} +``` +- بعد `getServerAccessToken` (در Server Component، read-only) فقط کوکی `access_token` معتبر فعلی را بخواند؛ تجدید توسط middleware/route قبل از رندر انجام شود. + +> **مهم:** هر کجای دیگر `nobat724` که توکن refresh ذخیره/استفاده می‌شود را هم بررسی کن (مثلاً جریان login که کوکی‌ها را ست می‌کند) تا توکن چرخش‌یافته‌ی جدید همیشه جایگزین قدیمی شود. + +## 🟡 وظیفه ۲ — صفحه‌بندی نظرات (`GET /api/v1/comments/{uuid}`) + +### وضعیت فعلی (`app/doctor/[slug]/page.js`) +```js +const resComments = await fetch(`${API_URL}/api/v1/comments/${doctor.uuid}`, { cache: "no-store" }); +const jsonComments = resComments.ok ? await resComments.json() : null; +comments = jsonComments?.data?.data; // آرایه — هنوز کار می‌کند +``` + +### تغییر +پاسخ حالا `{ data: { data: [...], meta: { totalRecords, totalPages, currentPage } } }` است و **پیش‌فرض ۵۰ نظر** برمی‌گرداند (قبلاً همه). `data.data` (آرایه) دست‌نخورده → کد فعلی **نمی‌شکند**، فقط حداکثر ۵۰ نظر نشان می‌دهد. + +### وظیفه +- اگر برای صفحه‌ی پزشک ۵۰ نظر کافی است (به‌علاوه‌ی schema/Review)، **هیچ تغییری لازم نیست** (فقط آگاه باش). +- اگر همه‌ی نظرات لازم است: یا `?limit=100` بفرست، یا «نمایش بیشتر»/صفحه‌بندی با `data.meta.totalPages` اضافه کن. `getDoctorComments` در `services/response.js` را هم در صورت نیاز با پارامتر `page`/`limit` تطبیق بده. +- اگر برای SEO/`Review` schema از تعداد کل نظر استفاده می‌کنی، آن را از `data.meta.totalRecords` بخوان (نه `length` آرایه‌ی ۵۰‌تایی). + +## نکات مهم + +- `lib/serverToken.js` در Server Component اجرا می‌شود → نمی‌تواند کوکی ست کند؛ ست‌کردن کوکی فقط در Route Handler/Server Action/middleware. (مسیر A این مشکل را کلاً حذف می‌کند.) +- `clinic-pro-tauri` تحت تأثیر rotation **نیست** چون از `/oauth/token` (grant_type=refresh_token، OAuth bundle) استفاده می‌کند نه `/oauth/token/refresh`. +- کدهای خطا (M21) مقدار wire ثابت دارند؛ هر منطقی که روی رشته‌ی `code` switch می‌کند سالم است. +- endpointهای owner-only جدید (`insurance/{id}`, `doctor-address/{id}`, `appointment-settings/*`) توسط سایت عمومی مصرف نمی‌شوند → بدون اثر. +- بعد از تغییر: `npm run lint` و `npm run build`. +```