From 08f5829a22cbccdd00c2bf0525018c30ccd0d8ce Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Mon, 15 Jun 2026 18:16:36 +0330 Subject: [PATCH] fix(auth): complete login by reading userinfo from response envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit oauth/userinfo returns { success, data: {...} }, so after the interceptor unwraps once the user lives at res.data, not res. getInfo checked res.uuid (undefined), so it never set the userInfo cookie or redirected — the /login page just sat there after entering the OTP. Read res.data, store the user object (with a username alias for mobile_number so the appointment flow keeps working), redirect via location.href, and show a toast instead of silently staying when userinfo fails. Co-Authored-By: Claude Opus 4.8 --- .claude/prompt/fix-login-session-userinfo.md | 132 ++++++++++++++++++ .../register/verificationPage/SendReq.js | 12 +- 2 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 .claude/prompt/fix-login-session-userinfo.md diff --git a/.claude/prompt/fix-login-session-userinfo.md b/.claude/prompt/fix-login-session-userinfo.md new file mode 100644 index 0000000..4f2c6fe --- /dev/null +++ b/.claude/prompt/fix-login-session-userinfo.md @@ -0,0 +1,132 @@ +# رفع ماندن صفحه‌ی لاگین بعد از وارد کردن کد 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 با پیام توصیفی. diff --git a/components/register/verificationPage/SendReq.js b/components/register/verificationPage/SendReq.js index d384c76..e24e4aa 100644 --- a/components/register/verificationPage/SendReq.js +++ b/components/register/verificationPage/SendReq.js @@ -84,20 +84,26 @@ function SendReq({ }) .then((res) => { setLoading(false); - if (res && res.uuid) { - Cookies.set("userInfo", JSON.stringify(res), cookieOptions); + const profile = res?.data; + if (profile?.uuid) { + const userInfo = { ...profile, username: profile.mobile_number }; + Cookies.set("userInfo", JSON.stringify(userInfo), cookieOptions); // اگر در صفحه appointment هستیم، به مرحله 3 (Detail) می‌رویم if (setStep) { setStep(3); } else { - window.location.pathname = "/"; + window.location.href = "/"; } + } else { + setIsError(true); + toast.error("دریافت اطلاعات کاربر ناموفق بود. دوباره تلاش کنید."); } }) .catch(() => { setLoading(false); setIsError(true); + toast.error("دریافت اطلاعات کاربر ناموفق بود. دوباره تلاش کنید."); }); };