fix(header): detect login state live from client cookie
The header decided between profile and login button only from the server-passed logged prop (read once via cookies() in StLayout), so a user who logged in client-side still saw ورود | ثبت نام until a hard reload. Seed isLogged from the server prop (correct first paint, no hydration mismatch) then sync it from the access_token cookie on mount and on every route change, so login/logout reflect immediately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# رفع نمایش «ورود | ثبتنام» در هدر با وجود لاگینبودن کاربر
|
||||
|
||||
## پروژه
|
||||
|
||||
`nobat724_front` — سایت عمومی. این کار **صرفاً frontend** است؛ backend تغییر نمیکند.
|
||||
|
||||
## زمینه
|
||||
|
||||
هدر سایت عمومی بسته به وضعیت لاگین، یا دکمهی «ورود | ثبت نام» را نشان میدهد یا آیکن پروفایل کاربر (`ProfileUser`). تصمیم بر اساس prop `logged` گرفته میشود که در `components/layout/StLayout.js` (Server Component) از روی کوکی `access_token` با `cookies()` خوانده و به هدر پاس داده میشود.
|
||||
|
||||
مشکل: کاربر بعد از لاگین موفق (کوکیهای `access_token`/`refresh_token`/`uuid`/`userInfo` با `js-cookie` ست میشوند)، **هنوز در هدر «ورود | ثبت نام» میبیند**. علت: `logged` فقط یکبار سمت سرور هنگام render محاسبه میشود و به ناوبریهای client-side (soft navigation) و کوکیهایی که سمت کلاینت ست شدهاند واکنش نشان نمیدهد؛ هدر تا یک hard-reload کامل وضعیت تازه را منعکس نمیکند و در بسیاری از مسیرها همان حالت اولیهی «لاگیننشده» باقی میماند.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
هدر باید وضعیت لاگین را **بهصورت زنده سمت کلاینت** هم تشخیص دهد: اگر کوکی `access_token` وجود دارد → پروفایل، وگرنه → دکمهی ورود. prop سرور (`logged`) بهعنوان مقدار اولیهی paint اول حفظ شود تا flash/hydration mismatch ندهد، ولی منبع نهایی تصمیم، کوکی سمت کلاینت باشد.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `components/layout/header/Content.js` | `"use client"`؛ بر اساس `logged` بین `ProfileUser` و دکمهی ورود سوییچ میکند — محل اصلی رفع |
|
||||
| `components/layout/StLayout.js` | Server Component؛ `logged={token}` را از `cookies().get("access_token")` میدهد (مقدار اولیه) |
|
||||
| `components/layout/index.js` | layout کلاینتیِ پنل/داشبورد؛ `logged` را بهصورت prop میگیرد |
|
||||
| `components/layout/header/index.js` | فقط prop را پاس میدهد |
|
||||
| `components/layout/header/profile/index.js` | `ProfileUser` — حالت لاگینشده |
|
||||
| `utils/index.js` | `removeToken()` — خروج |
|
||||
|
||||
## وضعیت فعلی (کد واقعی)
|
||||
|
||||
### `header/Content.js` — تصمیم فقط بر اساس prop سرور
|
||||
|
||||
```jsx
|
||||
"use client";
|
||||
// ...
|
||||
function Content({ matchedCity, name, logged }) {
|
||||
// ...
|
||||
{logged ? (
|
||||
<ProfileUser />
|
||||
) : (
|
||||
<Link href="/login">
|
||||
<Button variant="outlined" color="primary">ورود | ثبت نام</Button>
|
||||
</Link>
|
||||
)}
|
||||
}
|
||||
```
|
||||
|
||||
### `StLayout.js` — مقدار اولیه از کوکی سرور
|
||||
|
||||
```jsx
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get("access_token");
|
||||
// ...
|
||||
<Header matchedCity={matchedCity} name={name} logged={token} />
|
||||
```
|
||||
|
||||
> `token` یک شیء `{ name, value }` یا `undefined` است؛ پس `logged` truthy/undefined میشود. این فقط در render سرور محاسبه میشود.
|
||||
|
||||
### کوکیها سمت کلاینت ست میشوند (`SendReq.js`)
|
||||
|
||||
```js
|
||||
Cookies.set("access_token", response.access_token, cookieOptions); // js-cookie, غیر httpOnly
|
||||
```
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. تشخیص زندهی لاگین در `header/Content.js`
|
||||
|
||||
- یک state کلاینتی برای لاگینبودن اضافه کن که مقدار اولیهاش از prop `logged` (سرور) میآید تا paint اول درست باشد، سپس در `useEffect` با کوکی `access_token` همگام شود:
|
||||
|
||||
```jsx
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import Cookies from "js-cookie";
|
||||
// ...
|
||||
function Content({ matchedCity, name, logged }) {
|
||||
const [isLogged, setIsLogged] = useState(Boolean(logged));
|
||||
|
||||
useEffect(() => {
|
||||
setIsLogged(Boolean(Cookies.get("access_token")));
|
||||
}, []);
|
||||
// ...
|
||||
{isLogged ? <ProfileUser /> : <Link href="/login">...دکمه ورود...</Link>}
|
||||
}
|
||||
```
|
||||
|
||||
- `Boolean(logged)` مهم است: `logged` ممکن است شیء `{name,value}` یا `undefined` باشد؛ تبدیل به boolean کن.
|
||||
- چون مقدار اولیه از سرور میآید، hydration mismatch رخ نمیدهد؛ بعد از mount با کوکی واقعی کلاینت تصحیح میشود.
|
||||
|
||||
### ۲. واکنش به تغییر مسیر (اختیاری ولی توصیهشده)
|
||||
|
||||
- اگر لازم بود وضعیت بعد از login/logout بدون hard-reload بهروز شود، میتوانی `usePathname()` را به deps افزودن useEffect اضافه کنی تا با هر تغییر مسیر کوکی دوباره چک شود:
|
||||
|
||||
```jsx
|
||||
import { usePathname } from "next/navigation";
|
||||
const pathname = usePathname();
|
||||
useEffect(() => {
|
||||
setIsLogged(Boolean(Cookies.get("access_token")));
|
||||
}, [pathname]);
|
||||
```
|
||||
|
||||
> اگر فلوی فعلی login با `window.location.href` ریدایرکت میکند (hard reload)، paint اول سرور هم درست خواهد بود؛ این مرحله بیشتر برای ناوبریهای soft و logout است.
|
||||
|
||||
### ۳. سازگاری logout
|
||||
|
||||
- `ProfileUser`/`DetailProfile` هنگام logout `removeToken()` را صدا میزنند که کوکیها را پاک میکند. مطمئن شو بعد از logout، هدر دوباره دکمهی ورود را نشان میدهد (با همان مکانیزم کوکیخوانی + ناوبری/ریلود فعلی). اگر logout فقط state را عوض میکند و ریدایرکت ندارد، بررسی کن هدر بهروز شود.
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **هیچ تغییری در backend لازم نیست.** کوکی `access_token` غیر httpOnly است و سمت کلاینت با `js-cookie` قابلخواندن است.
|
||||
- **Hydration:** مقدار اولیهی state حتماً از prop `logged` سرور باشد، نه مستقیماً از `Cookies.get` در زمان render؛ خواندن کوکی فقط داخل `useEffect` (بعد از mount) تا SSR و کلاینت در paint اول یکی باشند.
|
||||
- **multi-domain / RTL** را خراب نکن؛ منطق `matchedCity`/`name` دستنخورده بماند.
|
||||
- این هدر هم در `StLayout` (صفحات عمومی، logged از سرور) و هم در `components/layout/index.js` (پنل/داشبورد، logged از prop) استفاده میشود؛ هر دو مسیر باید بعد از تغییر کار کنند (در هر دو، fallback به prop + همگامسازی با کوکی درست عمل میکند).
|
||||
- **تست:** `npm run build`؛ سپس دستی: قبل از لاگین → دکمهی «ورود | ثبت نام»؛ بعد از لاگین با `12345` و بازگشت به صفحه → آیکن پروفایل؛ بعد از logout → دوباره دکمهی ورود. سپس commit با پیام توصیفی.
|
||||
@@ -7,11 +7,19 @@ import Row from "./list/Row";
|
||||
import Col from "./list/col";
|
||||
import ButtonMenu from "./list/col/ButtonMenu";
|
||||
import BgGray from "./list/col/BgGray";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Cookies from "js-cookie";
|
||||
import ProfileUser from "./profile";
|
||||
|
||||
function Content({ matchedCity, name, logged }) {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [isLogged, setIsLogged] = useState(Boolean(logged));
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
setIsLogged(Boolean(Cookies.get("access_token")));
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -33,7 +41,7 @@ function Content({ matchedCity, name, logged }) {
|
||||
</div>
|
||||
</div>
|
||||
<Row name={name} />
|
||||
{logged ? (
|
||||
{isLogged ? (
|
||||
<ProfileUser />
|
||||
) : (
|
||||
<Link href="/login">
|
||||
|
||||
Reference in New Issue
Block a user