fix(auth): complete login by reading userinfo from response envelope

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 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-15 18:16:36 +03:30
co-authored by Claude Opus 4.8
parent eb3cb6ca10
commit 08f5829a22
2 changed files with 141 additions and 3 deletions
@@ -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 با پیام توصیفی.
@@ -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("دریافت اطلاعات کاربر ناموفق بود. دوباره تلاش کنید.");
});
};