# رفع ماندن صفحه‌ی لاگین بعد از وارد کردن کد OTP (نشست ساخته نمی‌شود) ## پروژه `nobat724_front` — سایت عمومی. **این کار صرفاً frontend است؛ backend درست کار می‌کند** (با curl تأیید شد: `send-code` → uuid، `verify-code` با `12345` → ۲۰۰، `oauth/token` → توکن، `oauth/userinfo` → `{ success, data: {...} }`). ## زمینه در صفحه‌ی `/login`، کاربر شماره موبایل را وارد می‌کند، کد `12345` (کد ثابت محیط dev) را می‌زند، ولی **صفحه روی `/login` می‌ماند** و وارد نمی‌شود. توکن گرفته می‌شود ولی نشست کامل نمی‌شود. علت دقیق: بعد از گرفتن توکن، `components/register/verificationPage/SendReq.js` تابع `getInfo` را صدا می‌زند که `request.getUserInfo()` (یعنی `GET /oauth/userinfo`) را فراخوانی می‌کند و فقط در صورت `res.uuid` کوکی `userInfo` را ست و ریدایرکت می‌کند. اما پاسخ `oauth/userinfo` به شکل `{ success: true, data: { uuid, mobile_number, roles, ... } }` است و interceptor در `services/api.js` **یک‌بار** پاسخ را باز می‌کند، پس داده‌ی واقعی در `res.data` است، نه `res` مستقیم. در نتیجه `res.uuid === undefined`، شرط `if (res && res.uuid)` رد می‌شود، **نه کوکی `userInfo` ست می‌شود و نه ریدایرکت اتفاق می‌افتد** → صفحه می‌ماند. > توجه: این همان pitfall «double-nesting» است که قبلاً در پروفایل و لیست بیمه هم دیده شد. اینجا پاسخ یک‌لایه envelope دارد (`{success, data}`)؛ بعد از interceptor، داده در `res.data`. ## مشکل / هدف تابع `getInfo` در `SendReq.js` باید کاربر را از `res.data` بخواند (نه `res`)، کوکی `userInfo` را با **شیء کاربر** (نه envelope) ست کند، و سپس ریدایرکت/پیشروی مرحله را انجام دهد. همچنین در صورت پاسخ نامعتبر، خطای واضح نمایش داده شود (نه ماندن بی‌صدا). ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `components/register/verificationPage/SendReq.js` | بعد از گرفتن توکن، userinfo می‌گیرد و کوکی `userInfo` را ست می‌کند — محل باگ | | `services/response.js` | `getUserInfo` → `GET /oauth/userinfo` با `requireAuth` | | `services/api.js` | interceptor که یک‌بار `response.data` را برمی‌گرداند | | `lib/auth.js` | `getUser()` — وجود کوکی `userInfo` را چک می‌کند (server-side) | | مصرف‌کننده‌های `userInfo` | `components/appointment/index.js`، `components/layout/sidebar/*`، `components/dashboard/*` — همگی `userInfo` را parse می‌کنند و انتظار شیء کاربر (`.uuid`، `.mobile_number`، `.representation_uuid`) دارند | ## وضعیت فعلی (کد واقعی) ### `SendReq.js` → `getInfo` (باگ‌دار) ```js const getInfo = (token, cookieOptions) => { request .getUserInfo({ headers: { Authorization: `Bearer ${token}` } }) .then((res) => { setLoading(false); if (res && res.uuid) { // ❌ uuid در res.data است نه res Cookies.set("userInfo", JSON.stringify(res), cookieOptions); // ❌ کل envelope ذخیره می‌شود if (setStep) { setStep(3); } else { window.location.pathname = "/"; } } // ❌ اگر uuid نبود، هیچ اتفاقی نمی‌افتد — صفحه می‌ماند، خطا هم نشان داده نمی‌شود }) .catch(() => { setLoading(false); setIsError(true); }); }; ``` ### قرارداد واقعی `GET /oauth/userinfo` (از `clinicpro/docs/api/auth.md`) ```json { "success": true, "data": { "id": 4766, "uuid": "550e8400-...", "mobile_number": "09123456789", "realName": "...", "roles": ["ROLE_USER"], "primary_role": "...", "doctor_uuid": "...", "representation_uuid": null } } ``` > بعد از interceptor: داده در `res.data`. هیچ فیلد `username` در پاسخ نیست؛ شماره موبایل `mobile_number` است. ## وظایف ### ۱. خواندن کاربر از `res.data` و ست‌کردن کوکی با شیء کاربر ```js const getInfo = (token, cookieOptions) => { request .getUserInfo({ headers: { Authorization: `Bearer ${token}` } }) .then((res) => { setLoading(false); const userInfo = res?.data; if (userInfo?.uuid) { Cookies.set("userInfo", JSON.stringify(userInfo), cookieOptions); if (setStep) { setStep(3); } else { window.location.href = "/"; } } else { setIsError(true); toast.error("دریافت اطلاعات کاربر ناموفق بود. دوباره تلاش کنید."); } }) .catch(() => { setLoading(false); setIsError(true); }); }; ``` - کوکی `userInfo` باید **شیء کاربر** (`res.data`) باشد تا مصرف‌کننده‌ها (`parsedData.uuid`, `.mobile_number`, `.representation_uuid`) درست کار کنند. - `window.location.href = "/"` به‌جای `window.location.pathname = "/"` (مطمئن‌تر برای reload کامل و خواندن کوکی‌های تازه server-side). - `toast` از قبل در این فلو استفاده می‌شود (در همان فایل بعد از رفع قبلی `import { toast } from "react-toastify"` اضافه شده) — اگر نبود اضافه کن؛ `ToastContainer` در `app/layout.js` mount است. ### ۲. سازگاری مصرف‌کننده‌های `username` - بعضی جاها (مثل `components/appointment/index.js`) `parsedData.username` خوانده می‌شود، ولی پاسخ `oauth/userinfo` فیلد `username` ندارد (فقط `mobile_number`). بررسی کن: - یا در `getInfo` هنگام ذخیره، یک فیلد `username` معادل `mobile_number` هم اضافه کن (سازگاری عقب‌رو، کم‌ریسک‌تر): ```js const userInfo = { ...res.data, username: res.data.mobile_number }; ``` - یا همه‌ی خواننده‌های `username` را به `mobile_number` مهاجرت بده. - **یکی را انتخاب کن**؛ گزینه‌ی اول (افزودن `username`) دیف کوچک‌تری دارد و فلوی appointment را نمی‌شکند. در گزارش ذکر کن. ### ۳. تأیید عدم ماندن بی‌صدا - اگر `verify-code` یا `oauth/token` خطا داد (۴۰۰)، از قبل (رفع قبلی) پیام toast نمایش داده می‌شود. مطمئن شو مسیر userinfo هم در خطا، `setIsError(true)` و پیام می‌دهد، نه سکوت. ## نکات مهم - **هیچ تغییری در backend لازم نیست.** پاسخ `oauth/userinfo` درست است؛ فقط استخراج سمت کلاینت غلط بود. - **interceptor:** همه‌ی `request.*` بدنه‌ی HTTP را یک‌بار باز می‌کنند → برای `{success, data}` داده در `res.data`. این الگو را با pitfallهای قبلی (پروفایل، بیمه) یکدست نگه‌دار. - **شکل کوکی `userInfo`:** باید شیء کاربر باشد (نه `{success, data}`)؛ مصرف‌کننده‌های متعدد (`appointment`, `sidebar`, `dashboard`, `lib/auth.getUser`) به این متکی‌اند. تغییر این شکل را در همه‌ی مصرف‌کننده‌ها بررسی کن. - **دو مسیر استفاده از `SendReq`:** صفحه‌ی مستقل `/login` (`RegisterPage` با `setStep={false}` → ریدایرکت به `/`) و فلوی نوبت (`setStep` تابع است → `setStep(3)`). هر دو باید بعد از رفع کار کنند. هر دو را تست کن. - **dev OTP:** کد ثابت `12345` است؛ هنگام تست با همین وارد شو. - **rate-limit:** backend روی `send-code` rate-limit دارد؛ اگر حین تست `ERR_RATE_LIMIT_001` گرفتی، چند دقیقه صبر کن یا شماره‌ی متفاوت بزن. - **تست:** `npm run build`؛ سپس دستی روی `/login`: موبایل + `12345` → باید کوکی‌های `access_token`/`refresh_token`/`uuid`/`userInfo` ست شوند و به `/` ریدایرکت شود. سپس commit با پیام توصیفی.