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>
8.5 KiB
رفع ماندن صفحهی لاگین بعد از وارد کردن کد 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 (باگدار)
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)
{
"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 و ستکردن کوکی با شیء کاربر
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.jsmount است.
۲. سازگاری مصرفکنندههای username
- بعضی جاها (مثل
components/appointment/index.js)parsedData.usernameخوانده میشود، ولی پاسخoauth/userinfoفیلدusernameندارد (فقطmobile_number). بررسی کن:- یا در
getInfoهنگام ذخیره، یک فیلدusernameمعادلmobile_numberهم اضافه کن (سازگاری عقبرو، کمریسکتر):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-coderate-limit دارد؛ اگر حین تستERR_RATE_LIMIT_001گرفتی، چند دقیقه صبر کن یا شمارهی متفاوت بزن. - تست:
npm run build؛ سپس دستی روی/login: موبایل +12345→ باید کوکیهایaccess_token/refresh_token/uuid/userInfoست شوند و به/ریدایرکت شود. سپس commit با پیام توصیفی.