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:
@@ -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("دریافت اطلاعات کاربر ناموفق بود. دوباره تلاش کنید.");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user