IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar7/36-dars27 daqiqa
Mundarija (30)

React Router himoyalangan marshrutlar: Navigate, ?keyin= qaytish, rollar va loader'da tekshirish

Qisqacha: Himoyalangan sahifalar bitta yo'lsiz layout marshrut ichiga olinadi. Uning komponenti foydalanuvchi kirmagan bo'lsa <Navigate to="/kirish?keyin=/joriy/yol" replace /> qaytaradi, kirgan bo'lsa — <Outlet />. Kirish sahifasi muvaffaqiyatdan keyin ?keyin= dagi manzilga qaytaradi — faqat / bilan boshlanadigan ichki yo'l bo'lsa (ochiq yo'naltirish hujumi). Loader komponentdan oldin ishlaydi, shuning uchun u ham o'zi tekshiradi. Rollar ham xuddi shunday: menyu rolga qarab, sahifa — 403. Bularning hammasi faqat ko'rinish: haqiqiy himoya — serverda.

Bu darsda

  • Himoyalangan marshrutlarni bitta yo'lsiz layout bilan o'raysiz va kirmagan foydalanuvchini /kirish ga yuborasiz.
  • ?keyin= bilan kirgandan keyin o'sha sahifaga qaytarasiz va ochiq yo'naltirish (open redirect) hujumini to'sasiz.
  • Sessiya tekshirilayotganda "miltillash" (kirish formasi bir zum ko'rinib yo'qolishi) ning oldini olasiz.
  • Rolga qarab menyuni va sahifani cheklaysiz (403).
  • Nega loader'da ham tekshirish kerakligini o'lchov bilan ko'rasiz va vazifalar ga /kirish sahifasini qo'shasiz.

Oldin bilishingiz kerak: React Router III: dasturiy navigatsiya, data mode — loader va action, React'da auth holati.

1. Nega bu kerak?

18-qismning oxirgi darsida vazifalar ga soxta kirish qo'shildi. U butun ilovani o'rardi: <Himoyalangan> kirmagan foydalanuvchiga ilova o'rniga kirish formasini ko'rsatardi. Router kelgach, uchta muammo chiqdi:

  1. Manzil yolg'on gapiradi. /statistika ochildi — manzil /statistika, ekranda esa kirish formasi. Kirish sahifasining o'z manzili yo'q, unga havola berib bo'lmaydi.
  2. Qaytish yo'q. Do'stingiz /vazifa/2 havolasini yubordi. Siz kirdingiz — va ro'yxatga tushdingiz. Qaysi vazifa edi? Havola esdan chiqdi.
  3. Loader himoyani chetlab o'tadi. Vazifa sahifasining loader'i komponentdan oldin ishlaydi. Kirmagan foydalanuvchi uchun ham serverga so'rov ketadi (kanon buni o'lchadi — «Loader'da ham tekshirish» bo'limida).

Hayotiy o'xshatish — bino qorovuli. Yaxshi qorovul ruxsatnomasiz odamni eshikdan qaytaradi, lekin "qaysi xonaga kelgandingiz?" deb yozib qo'yadi: ruxsatnoma olgach, odam to'g'ri o'sha xonaga boradi. Har xona eshigida esa (loader) yana bir qulf bor — kimdir derazadan kirib qolmasin.

Diqqat: Bu dars — ko'rinish himoyasi. Brauzerdagi kodni har kim o'qiydi va o'zgartiradi: DevTools'da localStorage ga ism yozib qo'yish kifoya. Ma'lumotni haqiqatan himoya qiladigan joy — server, u har so'rovda sessiyani tekshiradi (Session-based auth, 28-qism). Bugungi kod foydalanuvchiga qulaylik beradi va keraksiz so'rovlarni to'xtatadi, xolos.

2. Himoyalangan marshrut — yo'lsiz layout

2.1 Qorovul komponenti

03-darsdagi yo'lsiz layout — path siz marshrut. U manzilga hech narsa qo'shmaydi, faqat bolalarini o'raydi. Qorovul uchun ideal joy: himoyalangan sahifalar uning bolalari bo'ladi.

Ingliz tilidagi maqolalarda bu komponent ProtectedRoute yoki RequireAuth deb ataladi. Natija oynasida auth — oddiy Context, ism useState da:

jsx
import { createContext, use, useState } from "react";
import {
  createMemoryRouter,
  Link,
  Navigate,
  Outlet,
  RouterProvider,
  useLocation,
  useNavigate,
} from "react-router";

const AuthContext = createContext(null);

function Shell() {
  const { pathname } = useLocation();
  return (
    <div>
      <p>Manzil: {pathname}</p>
      <Outlet />
    </div>
  );
}

// Qorovul: kirmagan — /kirish ga, kirgan — bola sahifa
function RequireAuth() {
  const { user } = use(AuthContext);
  if (user === null) {
    return <Navigate to="/kirish" replace />;
  }
  return <Outlet />;
}

function Login() {
  const { setUser } = use(AuthContext);
  const navigate = useNavigate();
  function logIn() {
    setUser("Malika");
    navigate("/kabinet", { replace: true });
  }
  return <button onClick={logIn}>Kirish</button>;
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    children: [
      { index: true, element: <Link to="/kabinet">Kabinet</Link> },
      { path: "kirish", element: <Login /> },
      {
        // path yo'q — yo'lsiz layout
        element: <RequireAuth />,
        children: [
          { path: "kabinet", element: <p>Kabinet: buyurtmalar</p> },
        ],
      },
    ],
  },
]);

export default function App() {
  const [user, setUser] = useState(null);
  return (
    <AuthContext value={{ user, setUser }}>
      <RouterProvider router={router} />
    </AuthContext>
  );
}

Ekranda:

text
Manzil: /kabinet
Kabinet: buyurtmalar

Oqim: «Kabinet» → qorovul "kirmagan" deb /kirish ga yubordi → «Kirish» → kabinet. Ikki joyda replace: yo'naltirishda va kirishdan keyin. Navigatsiya darsidagi qoida — yo'naltirish tarixni o'stirmasin. Aks holda «Orqaga» foydalanuvchini qorovulga qaytarib, u yana /kirish ga otadi.

Endi yangi himoyalangan sahifa qo'shish — bitta qator: RequireAuth ning children iga. Har sahifaga alohida if (user === null) yozilmaydi va hech biri esdan chiqmaydi.

Maslahat: <AuthContext value={…}> — React 19 da Context'ning o'zi provider (Context). Auth provider router'dan tashqarida: kirish holati sahifa almashganda yo'qolmasin.

2.2 ?keyin= — qaytish manzili

Oqimda kamchilik bor: kirgandan keyin har doim /kabinet ga. Foydalanuvchi /buyurtma/12 ni ochmoqchi bo'lgan bo'lsa-chi? Qorovul qaytish manzilini URL'ga yozib qo'yadi: /kirish?keyin=%2Fbuyurtma%2F12. Inglizcha loyihalarda ?from=, ?redirect=, ?next= deyiladi. Kirish sahifasi uni useSearchParams bilan o'qiydi:

jsx
import { createContext, use, useState } from "react";
import {
  createMemoryRouter,
  Link,
  Navigate,
  Outlet,
  RouterProvider,
  useLocation,
  useNavigate,
  useSearchParams,
} from "react-router";

const AuthContext = createContext(null);

function Shell() {
  const { pathname, search } = useLocation();
  return (
    <div>
      <p>Manzil: {pathname + search}</p>
      <Outlet />
    </div>
  );
}

function RequireAuth() {
  const { user } = use(AuthContext);
  const { pathname, search } = useLocation();
  if (user === null) {
    const back = new URLSearchParams({ keyin: pathname + search });
    return <Navigate to={`/kirish?${back}`} replace />;
  }
  return <Outlet />;
}

function Login() {
  const { setUser } = use(AuthContext);
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();
  function logIn() {
    setUser("Malika");
    navigate(searchParams.get("keyin") ?? "/", { replace: true });
  }
  return <button onClick={logIn}>Kirish</button>;
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    children: [
      {
        index: true,
        element: <Link to="/buyurtma/12">12-buyurtma</Link>,
      },
      { path: "kirish", element: <Login /> },
      {
        element: <RequireAuth />,
        children: [
          {
            path: "buyurtma/:id",
            element: <p>Buyurtma: osh, choy</p>,
          },
        ],
      },
    ],
  },
]);

export default function App() {
  const [user, setUser] = useState(null);
  return (
    <AuthContext value={{ user, setUser }}>
      <RouterProvider router={router} />
    </AuthContext>
  );
}

Ekranda:

text
Manzil: /buyurtma/12
Buyurtma: osh, choy

Oraliq nuqta (ekran("keyin=")) kirish sahifasidagi manzilni tekshirdi: /kirish?keyin=%2Fbuyurtma%2F12. %2F — / ning URL ichidagi kodi: URLSearchParams qiymatni o'zi kodlaydi (URL va URLSearchParams). Kirishdan keyin — aynan /buyurtma/12.

Butun oqim bitta rasmda — vazifalar da shunday bo'ladi:

flowchart TD
  A["/vazifa/2 ochildi"] --> B{"kirganmi?"}
  B -- ha --> C["vazifa sahifasi"]
  B -- yo'q --> D["/kirish?keyin=%2Fvazifa%2F2<br/>(replace)"]
  D --> E["ism kiritildi"]
  E --> F{"keyin — ichki yo'lmi?"}
  F -- ha --> G["/vazifa/2 (replace)"]
  F -- yo'q --> H["/ (bosh sahifa)"]

Ikki romb — ikki tekshiruv. Birinchisi — qorovul (va loader). Ikkinchisi — ?keyin= qiymati.

Bu kod ishlaydi, lekin unda xavfli teshik bor: searchParams.get("keyin") tekshirilmasdan navigate ga berildi. Nima bo'lishi mumkinligi — «Hujumchi nigohi» bo'limida.

Tekshirib ko'ring: Nega qaytish manzili location.state da emas, URL'da (?keyin=)? Oldingi darsdan bilasiz — navigate ga state berish mumkin.

Javob

state mo'rt (navigatsiya darsi): yangi tabda, nusxalangan havolada va ba'zi qayta yuklashlarda yo'qoladi. Kirish sahifasini foydalanuvchi parolni tiklash xatidan, boshqa tabdan ham ochishi mumkin. ?keyin= esa manzilning bir qismi — sahifa yangilansa ham, havola boshqa joyda ochilsa ham saqlanadi. Bir narsa uchun to'lov: qiymatni har kim o'zgartira oladi, shuning uchun tekshiriladi.

2.3 Kirgan foydalanuvchi /kirish ni ochsa

Teskari holat: Malika allaqachon kirgan, lekin xatcho'pdan /kirish ni ochdi. Unga forma ko'rsatish ma'nosiz — ?keyin= dagi (yoki bosh) sahifaga yuboriladi. vazifalar dagi KirishSahifasi aynan shunday (kanondan aynan):

tsx
// KirishSahifasi.tsx — "/kirish" (19/#07). Kirgan bo'lsa (shu tabda
// forma orqali yoki BOSHQA tabda) — ?keyin= dagi sahifaga, qaytish
// tarixda qolmasin (replace)
export function KirishSahifasi() {
  const { foydalanuvchi } = useAuth();
  const [parametrlar] = useSearchParams();
  if (foydalanuvchi !== null) {
    return (
      <Navigate to={keyingiManzil(parametrlar.get("keyin"))} replace />
    );
  }
  return (
    <>
      <title>Kirish — Vazifalar</title>
      <KirishFormasi />
    </>
  );
}

Qiziq tomoni: forma kirgandan keyin o'zi navigate chaqirmaydi. Auth holati o'zgardi → KirishSahifasi qayta chizildi → foydalanuvchi !== null → <Navigate>. Boshqa tabda kirilsa ham shu yo'l ishlaydi: BroadcastChannel xabari keldi, holat o'zgardi, sahifa o'zi yo'naltirdi. Bitta joyda bitta qoida.

3. Miltillash: "hali bilmayman" holati

3.1 Muammo

Haqiqiy ilovada "kirganmi?" degan savolga darhol javob yo'q. Sessiya serverda: sahifa ochilganda GET /api/men so'raladi, javob 200 ms da keladi. Shu 200 ms da user — null. Qorovul "kirmagan" deb /kirish ga yuboradi, keyin javob keladi — "kirgan ekan". Natija: kirish formasi bir zum ko'rinadi va yo'qoladi (yoki foydalanuvchi umuman kirish sahifasida qolib ketadi). Bu miltillash (flicker).

Davosi — uchinchi holat. null ikki ma'noni bildirmasin: "kirmagan" va "hali bilmayman" alohida bo'lsin:

jsx
import { createContext, use, useEffect, useState } from "react";
import {
  createMemoryRouter,
  Navigate,
  Outlet,
  RouterProvider,
} from "react-router";

const AuthContext = createContext(null);

// Soxta server: sessiya bor, javob 200 ms da
const fetchMe = () =>
  new Promise((resolve) => setTimeout(() => resolve("Malika"), 200));

function RequireAuth() {
  const { status } = use(AuthContext);
  if (status === "tekshirilmoqda") {
    return <p role="status">Tekshirilmoqda…</p>;
  }
  if (status === "mehmon") {
    return <Navigate to="/kirish" replace />;
  }
  return <Outlet />;
}

const router = createMemoryRouter(
  [
    { path: "/kirish", element: <p>Kirish formasi</p> },
    {
      element: <RequireAuth />,
      children: [{ path: "/kabinet", element: <p>Kabinet</p> }],
    },
  ],
  { initialEntries: ["/kabinet"] },
);

export default function App() {
  const [status, setStatus] = useState("tekshirilmoqda");
  useEffect(() => {
    fetchMe().then((name) => setStatus(name ? "kirgan" : "mehmon"));
  }, []);
  return (
    <AuthContext value={{ status }}>
      <RouterProvider router={router} />
    </AuthContext>
  );
}

Ekranda:

text
Kabinet

200 ms davomida — "Tekshirilmoqda…", keyin to'g'ridan kabinet. Kirish formasi umuman ko'rinmadi. Tur nuqtai nazaridan bu discriminated union: "tekshirilmoqda" | "mehmon" | "kirgan". Har holat aniq, null ning ikki ma'nosi yo'q.

vazifalar da bu muammo yo'q: soxta auth ismni localStorage dan sinxron o'qiydi, birinchi chizishdayoq javob tayyor. Lekin haqiqiy auth (28-qism) bilan shu uch holat kerak bo'ladi.

3.2 <Navigate> va bo'sh birinchi kadr

Miltillashning ikkinchi turini kanon o'lchab topdi. Navigatsiya darsidagi ogohlantirishni eslang: <Navigate> manzilni effektda almashtiradi. Ya'ni birinchi kadrda u hech narsa chizmaydi, manzil keyin almashadi, kirish sahifasi esa undan keyin chiziladi. Kanon o'lchovi: sahifa yuklanganda kirish formasi yo'q edi — bir kadr bo'sh ekran.

Kanon yechimi — yo'naltirish bilan birga formani shu kadrning o'zida chizish (kanondan aynan):

tsx
export function Himoyalangan() {
  const { foydalanuvchi } = useAuth();
  const { pathname, search } = useLocation();
  // Layout (Qobiq) konteksti shu marshrut orqali bolalarga o'tadi:
  // Outlet konteksti faqat bevosita bolaga yetadi
  const kontekst = useOutletContext();
  if (foydalanuvchi === null) {
    // replace: Orqaga himoyalangan sahifaga qaytib, yana shu yerga
    // otilmasin. <Navigate> manzilni EFFEKTDA almashtiradi —
    // birinchi kadr bo'sh qolardi (o'lchandi: sahifa yuklanganda
    // forma yo'q edi); forma shu kadrdayoq ko'rinsin
    return (
      <>
        <Navigate to={kirishManzili(pathname + search)} replace />
        <KirishFormasi />
      </>
    );
  }
  return <Outlet context={kontekst} />;
}

Birinchi kadrda: forma ko'rinadi, manzil hali /statistika. Effekt ishlaydi: manzil /kirish?keyin=… ga almashadi, endi KirishSahifasi o'sha formani chizadi. Foydalanuvchi uchun ekran o'zgarmaydi — bo'sh kadr yo'q.

Ikki mayda, lekin muhim detal:

  • useOutletContext() — Qobiq ning konteksti (sarlavhaRef) qorovul orqali bolalarga qayta uzatiladi. 03-darsdagi tuzoq: Outlet konteksti faqat bevosita bolaga yetadi.
  • KirishFormasi da fokus endi useLayoutEffect bilan (useLayoutEffect): birinchi chizishdayoq, bo'yashdan oldin fokus ism maydonida. Bobur (faqat klaviatura) sahifa ochilishi bilan yozishni boshlay oladi.

4. Rollar: kim nimani ko'radi

4.1 Rol — qo'shimcha shart

«Bahor» da uch xil odam: mehmon (kirmagan), oshpaz va admin (Jasur aka). Oshpaz buyurtmalarni ko'radi, narxlarni faqat admin o'zgartiradi. Rolga asoslangan kirish (role-based access control, RBAC) — sahifa va tugmalar foydalanuvchining roliga qarab ochiladi.

Qorovul ikkinchi shartni oladi: "kirganmi?" dan keyin "roli mosmi?". Mos kelmasa — /kirish emas, 403 ("ruxsat yo'q"): foydalanuvchi kirgan, qayta kirish yordam bermaydi. Menyu ham rolga qarab:

jsx
import { createContext, use, useState } from "react";
import {
  createMemoryRouter,
  Link,
  Navigate,
  Outlet,
  RouterProvider,
} from "react-router";

const AuthContext = createContext(null);

const USERS = {
  ali: { name: "Ali", roles: ["oshpaz"] },
  jasur: { name: "Jasur aka", roles: ["oshpaz", "admin"] },
};

const can = (user, role) => user?.roles.includes(role) ?? false;

function RequireRole({ role }) {
  const { user } = use(AuthContext);
  if (user === null) return <Navigate to="/kirish" replace />;
  if (!can(user, role)) return <p>403: bu sahifa {role} uchun</p>;
  return <Outlet />;
}

function Shell() {
  const { user, setUser } = use(AuthContext);
  return (
    <div>
      <button onClick={() => setUser(USERS.ali)}>
        Ali (oshpaz)
      </button>{" "}
      <button onClick={() => setUser(USERS.jasur)}>
        Jasur aka (admin)
      </button>
      <nav>
        <Link to="/buyurtmalar">Buyurtmalar</Link>
        {/* menyuda — faqat ruxsat borlari */}
        {can(user, "admin") && <Link to="/narxlar"> Narxlar</Link>}
      </nav>
      <Outlet />
    </div>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    children: [
      { path: "kirish", element: <p>Kirish formasi</p> },
      {
        element: <RequireRole role="oshpaz" />,
        children: [
          { path: "buyurtmalar", element: <p>Buyurtmalar: 3 ta</p> },
        ],
      },
      {
        element: <RequireRole role="admin" />,
        children: [
          { path: "narxlar", element: <p>Narxlar: osh 35 000</p> },
        ],
      },
    ],
  },
]);

export default function App() {
  const [user, setUser] = useState(null);
  return (
    <AuthContext value={{ user, setUser }}>
      <RouterProvider router={router} />
    </AuthContext>
  );
}

Ekranda:

text
Ali (oshpaz) Jasur aka (admin)
Buyurtmalar Narxlar
Narxlar: osh 35 000

Harakat qatori: Ali tanlandi — menyuda faqat «Buyurtmalar», u ochildi (ekran qadami tekshirdi). «Narxlar» ni Ali bosa olmaydi: havolaning o'zi yo'q. Jasur aka tanlangach «Narxlar» paydo bo'ldi va sahifa ochildi. Admin uchun rol tekshiruvi (RBAC) serverda ham takrorlanadi.

4.2 Menyuni yashirish — himoya emas

Ali menyuda «Narxlar» ni ko'rmaydi. Lekin manzil satriga /narxlar ni o'zi yozishi mumkin. Shuning uchun ikkala qatlam kerak:

Qatlam Nima qiladi Nimadan himoya qilmaydi
menyu (can(user, "admin") &&) ortiqcha tugma ko'rsatmaydi qo'lda yozilgan manzildan
marshrut (RequireRole) sahifani ochmaydi (403) brauzer kodini o'zgartirishdan
server ma'lumotni bermaydi, o'zgartirishga yo'l qo'ymaydi — bu oxirgi qatlam

Rol tekshiruvini bitta funksiyaga (can) yig'ish — keyin "admin oshpaz ishini ham qila oladi" kabi qoidani bitta joyda o'zgartirasiz. Katta ilovalarda rol o'rniga ruxsatlar (permissions) tekshiriladi: can(user, "narx:tahrir"). Rol — ruxsatlar to'plami.

Diqqat: vazifalar kanonida rollar yo'q: soxta auth'da faqat ism bor, rolni beradigan server yo'q. Rolni brauzerda o'zimiz "o'ylab topsak", bu o'rganish uchun ham noto'g'ri namuna bo'lardi. Rollar haqiqiy auth bilan birga — 28-qismda. Yuqoridagi naqsh — o'shanda ishlatiladigan tuzilma.

5. Loader'da ham tekshirish

5.1 Loader qorovuldan oldin ishlaydi

Data mode darsidan bilasiz: router sahifani chizishdan oldin loader'larni chaqiradi. Qorovul esa komponent — u loader tugagandan keyin chiziladi. Demak, kirmagan foydalanuvchi /vazifa/1 ni ochsa, loader serverga so'rov yuboradi, keyin qorovul uni /kirish ga otadi. Ko'ramiz:

jsx
import {
  createMemoryRouter,
  Navigate,
  Outlet,
  RouterProvider,
  useLoaderData,
} from "react-router";

const user = null; // kirmagan

function RequireAuth() {
  if (user === null) return <Navigate to="/kirish" replace />;
  return <Outlet />;
}

function Task() {
  return <p>{useLoaderData()}</p>;
}

const router = createMemoryRouter(
  [
    { path: "/kirish", element: <p>Kirish formasi</p> },
    {
      element: <RequireAuth />,
      children: [
        {
          path: "/vazifa/:id",
          element: <Task />,
          loader: ({ params }) => {
            console.log("serverga so'rov: vazifa", params.id);
            return "Non olish";
          },
        },
      ],
    },
  ],
  { initialEntries: ["/vazifa/1"] },
);

export default function App() {
  return <RouterProvider router={router} />;
}

Konsolda:

text
serverga so'rov: vazifa 1

Ekranda:

text
Kirish formasi

Foydalanuvchi kirish sahifasini ko'rdi — lekin so'rov ketib bo'ldi. Haqiqiy serverda u 401 qaytaradi (zarar yo'q), lekin trafik behuda, server logida xatolar, ba'zan konsolda qizil qator. Kanon e2e tekshiruvi aynan shuni o'lchaydi: kirmagan /vazifa/1 — serverga so'rovlar ro'yxati bo'sh bo'lishi kerak.

5.2 Davosi: loader o'zi /kirish ga yo'naltiradi — replace() bilan

Loader hook ishlata olmaydi (u komponent emas), lekin auth holati React'dan tashqarida ham bor — localStorage da. Loader uni o'qiydi va yo'naltirish javobini otadi (throw). Qaysi javobni? Data mode darsida ikkalasini ko'rgansiz: redirect() va replace(). Farqi — tarixda: redirect() yangi yozuv qo'shadi (push), replace() joriy yozuvni almashtiradi.

Kanon avval redirect() yozgan edi. Kanon tekshiruvi buni haqiqiy Chrome'da o'lchadi: kirmagan foydalanuvchi /vazifa/1 ni yangi tabda to'g'ridan ochadi, /kirish chiqadi, keyin brauzerning «Orqaga» tugmasi. Tab about:blank dan boshlanadi (history.length 1). Natija (natijalar/07-kirish-replace.txt):

Qadam redirect() (push) replace()
/vazifa/1 ochildi → loader /kirish?keyin=%2Fvazifa%2F1, tarix 3 /kirish?keyin=%2Fvazifa%2F1, tarix 2
«Orqaga» 1 /vazifa/1 → loader → yana /kirish about:blank — tabdan chiqdi
«Orqaga» 2, 3, 4 har safar yana /kirish —

Nima bo'ldi? React Router birinchi yuklanishdagi loader yo'naltirishini ham push qiladi. Tarixda /vazifa/1 yozuvi qoladi. «Orqaga» uni ochadi, loader yana /kirish ga otadi — foydalanuvchi tabdan «Orqaga» bilan chiqa olmaydi. replace() bilan tarix toza: «Orqaga» oldingi saytga olib ketadi. Qorovuldagi <Navigate replace> (/statistika) ham aynan shunday ishlaydi (o'sha o'lchov). Qoida bitta: yo'naltirish — replace, u komponentda bo'ladimi yoki loader'da.

Kanon kodi (aynan; izoh qatorlari qisqa bo'lingan):

ts
// 19/#07: loader komponentdan (Himoyalangan) OLDIN ishlaydi —
// kirmagan foydalanuvchi uchun ham serverga so'rov ketardi
// (o'lchandi). Shuning uchun o'zi ham tekshiradi va /kirish ga
// yo'naltiradi. replace(), redirect() emas: redirect yozuv
// QO'SHADI — «Orqaga» himoyalangan manzilga, loader yana /kirish
// ga — tabdan chiqib bo'lmasdi (o'lchandi)
function kirganmi(request: Request): void {
  if (foydalanuvchiniOqi(foydalanuvchiMatni()) === null) {
    const { pathname, search } = new URL(request.url);
    throw replace(kirishManzili(pathname + search));
  }
}

export async function vazifaLoader({
  params,
  request,
}: LoaderFunctionArgs): Promise<VazifaMalumoti> {
  kirganmi(request);
  const raqam = raqaminiOl(params.id);
  // …

request.url — loader'ga kelgan to'liq manzil, undan ?keyin= uchun yo'l olinadi. foydalanuvchiniOqi — 18-qismdagi o'sha funksiya: localStorage dagi matnni tekshirib o'qiydi. kirishManzili — qorovul bilan umumiy funksiya: ikkala joy bir xil ?keyin= yasaydi.

5.3 Nega router middleware emas?

React Router 8 da middleware bor — marshrutga middleware: [fn] beriladi. U shu marshrut va bolalarining hamma loader'laridan oldin ishlaydi. Himoya uchun ideal ko'rinadi: bitta funksiya, hamma loader himoyalangan.

ts
// O'LCHANDI, KANONGA KIRMADI (natijalar/07-middleware.txt)
const authMiddleware = () => {
  if (foydalanuvchiniOqi(foydalanuvchiMatni()) === null) {
    throw redirect("/kirish");
  }
};
// yo'lsiz layout: { middleware: [authMiddleware], children: [...] }

Kanon sinab ko'rdi (natijalar/07-middleware.txt, aynan):

text
kirgan=true /: initialized darhol=false, joy=/
   10 ms dan keyin: initialized=true, joy=/

Middleware bo'lsa, ro'yxat sahifasi (/, loader'i yo'q) ham darhol tayyor emas: initialized=false. Router middleware'ni asinxron kutadi, birinchi kadrda esa hydrateFallback ("Yuklanmoqda…") chiqadi. Data mode darsida ro'yxat sahifasidan loader'ni aynan shu sababli olib tashlagan edik — u keshdan darhol chizilsin (CLS 0). Kanon qarori: himoya — komponent qorovul (birinchi kadr sinxron) + loader'ga ega sahifada loader ichida tekshiruv.

Bu middleware'ni kamsitish emas. Server render'li ilovada (framework mode, 06-dars) middleware — himoyaning eng toza joyi: u serverda ishlaydi va birinchi kadr muammosi yo'q.

6. Vazifalar qadami: /kirish va himoyalangan layout (holat 07)

6.1 Nima o'zgaradi

bash
git switch -c feat/himoyalangan-marshrutlar
  1. main.tsx dan <Himoyalangan> o'rami olib tashlanadi — himoya endi marshrutlarda.
  2. marshrutlar.tsx: /kirish — ochiq sahifa; qolgan hammasi — yo'lsiz layout Himoyalangan ichida.
  3. Himoyalangan — «<Navigate> va bo'sh birinchi kadr» bo'limidagi kod; KirishSahifasi — «Kirgan foydalanuvchi /kirish ni ochsa» bo'limidagi.
  4. keyingiManzil() — ?keyin= tekshiruvi («Hujumchi nigohi»).
  5. vazifaLoader — kirganmi(request).
  6. Qobiq — kirmagan bo'lsa bo'limlar menyusi va skip link chizilmaydi.

6.2 Marshrutlar

marshrutlar.tsx (kanon, qisqartirilgan):

diff
     children: [
-      // index — ota manzilining o'zi ("/")
-      { index: true, element: <RoyxatSahifasi /> },
-      …
+      // 19/#07: ochiq sahifa — kirish
+      { path: "kirish", element: <KirishSahifasi /> },
+      {
+        // Yo'lsiz (pathless) layout marshrut: manzilga hech narsa
+        // qo'shmaydi, faqat bolalarini o'raydi — kirmagan bo'lsa
+        // /kirish
+        element: <Himoyalangan />,
+        children: [
+          // index — ota manzilining o'zi ("/")
+          { index: true, element: <RoyxatSahifasi /> },
+          { path: "statistika", element: <StatistikaSahifasi /> },
+          { path: "sozlamalar", element: <SozlamalarSahifasi /> },
+          { path: "vazifa/:id", … },
+        ],
+      },
     ],

Qobiq hamon eng tashqarida: /kirish ham uning ichida (foydalanuvchi menyusi, sarlavha). Lekin Qobiq endi useAuth() ni o'qiydi va kirmagan bo'lsa bo'limlar menyusini chizmaydi — kirish sahifasida «Statistika» havolasi ma'nosiz.

Qobiq.tsx dagi o'zgarish mohiyati (kanon):

diff
+  // 19/#07: kirmagan — bo'limlar menyusi yo'q (hammasi
+  // himoyalangan)
+  const kirgan = useAuth().foydalanuvchi !== null;
...
-      <nav className="bolimlar" aria-label="Bo'limlar">
+      {kirgan && (
+        <nav className="bolimlar" aria-label="Bo'limlar">

6.3 Tekshiruvlar

bash
git add -A
git commit

Commit (kanon): feat: /kirish, Himoyalangan layout marshrut, ?keyin= (open redirect himoyasi).

  • npm test — node:test 183 (+1: keyingiManzil, olti yomon qiymat bilan), Vitest 56 (+1: loader kirmaganda /kirish ga). lint, format:check, tip, knip — exit 0.
  • Brauzer: 159/159, 72/72, 19-qism — 46/46. Yangi sakkiztasi (aynan):
text
✅ kirmagan: /statistika → /kirish?keyin=..., menyu yo'q, fokus ismda: ["/kirish?keyin=%2Fstatistika%3Fx%3D1",0,"kirish-ism"]
✅ kirdi: o'sha sahifaga qaytdi (replace — tarix o'smadi): ["/statistika?x=1",0]
✅ kirgan /kirish ni ochsa — ro'yxatga: "/"
✅ kirmagan /vazifa/1: loader serverga so'ramadi, /kirish ga: [[],"/kirish?keyin=%2Fvazifa%2F1"]
✅ open redirect yo'q: ?keyin=//yomon.example → ilova ichida «/»: [true,"/"]
✅ himoya: CSP buzilishi yo'q: []
✅ kirmagan /vazifa/1 yangi tabda: tarix 2 yozuv, «Orqaga» — tabdan chiqdi (/kirish ga qaytmadi): [2,"about:blank"]
✅ konsolda xato yo'q: []

Birinchi qatorda uchta da'vo birga: manzil ?keyin= bilan (%3F — ?, %3D — =, so'rov qatori ham saqlangan), bo'limlar menyusi 0 ta, fokus ism maydonida. Ikkinchisi — kirgach /statistika?x=1 ga qaytdi, tarix uzunligi o'zgarmadi. To'rtinchisi — «Loader'da ham tekshirish» bo'limining isboti: serverga so'rovlar ro'yxati bo'sh ([]). Yettinchisi — replace() ning isboti: yangi tabda tarix 2 yozuv (about:blank va /kirish), «Orqaga» — about:blank. redirect() bilan shu tekshiruv [3,"/kirish?keyin=%2Fvazifa%2F1"] bilan yiqiladi. Diff (holat 05 dan): 12 fayl, +230 −70.

7. Hujumchi nigohi

Ochiq yo'naltirish (open redirect). «?keyin= — qaytish manzili» bo'limidagi kodni hujumchi ko'zi bilan o'qing. Kirish sahifasi URL'dagi qiymatni tekshirmasdan navigate ga beradi. Hujumchi Telegram'da havola tarqatadi:

https://sizning-sayt.example/kirish?keyin=https://bahor-login.example/parol

Domen — sizniki, odam ishonadi. Kiradi — va brauzer uni begona saytga olib ketadi. U yerda sizning saytingizga o'xshash sahifa "sessiya tugadi, parolni qayta kiriting" deydi. navigate("https://…") to'liq manzilni ham qabul qiladi, //bahor-login.example (protokolsiz) ham — brauzer uni boshqa domen deb tushunadi.

Himoya — "faqat shu ilova ichidagi yo'l" qoidasi (kanondan aynan):

js
function keyingiManzil(keyin) {
  if (
    keyin === null ||
    !keyin.startsWith("/") ||
    /^\/[/\\]/.test(keyin)
  ) {
    return "/";
  }
  return keyin;
}

for (const keyin of [
  "/vazifa/2?x=1",
  null,
  "https://yomon.example",
  "//yomon.example/x",
  "/\\yomon.example",
  "javascript:alert(1)",
]) {
  console.log(JSON.stringify(keyin), "→", keyingiManzil(keyin));
}

Konsolda:

text
"/vazifa/2?x=1" → /vazifa/2?x=1
null → /
"https://yomon.example" → /
"//yomon.example/x" → /
"/\\yomon.example" → /
"javascript:alert(1)" → /

(Kanonda funksiya TypeScript'da, keyin: string | null bilan; bu yerda tiplarsiz.) Uch qoida:

  • / bilan boshlanmasa (https://…, javascript:…) — rad.
  • // bilan boshlansa — rad: bu "protokolga nisbiy" manzil, boshqa domen.
  • /\ bilan boshlansa — rad: brauzerlar \ ni / deb o'qiydi, /\yomon.example ham boshqa domen.

Shubhali hamma narsa — /. Foydalanuvchi bosh sahifaga tushadi, xavf yo'q. Kanonda bu olti qiymat — node:test da, brauzer e2e'da ham ?keyin=//yomon.example sinaladi.

Maslahat: Ruxsat ro'yxati (allowlist) — "nima mumkin" ni aniq aytish — har doim taqiq ro'yxatidan (blocklist) ishonchliroq. keyingiManzil "https bo'lmasin" demaydi, "faqat /x ko'rinishi" deydi: hujumchi o'ylab topadigan yangi hiyla (HTTPS:, %2F%2F) ham avtomatik rad etiladi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
kirgandan keyin «Orqaga» — yana kirish sahifasi yo'naltirish replace siz <Navigate replace>, navigate(…, { replace: true })
kirish formasi bir zum miltillaydi "hali bilmayman" ham null uch holat: tekshirilmoqda, mehmon, kirgan
kirgandan keyin har doim bosh sahifa qaytish manzili saqlanmagan ?keyin=
foydalanuvchi begona saytga ketdi ?keyin= tekshirilmagan keyingiManzil — faqat ichki yo'l
kirmagan foydalanuvchi uchun API so'rovi loader qorovuldan oldin loader'da throw replace(…)
yangi tabda «Orqaga» — yana va yana /kirish loader'da redirect() (tarixga push) replace() — joriy yozuvni almashtiradi
himoyalangan sahifada sarlavhaRef — undefined qorovul kontekstni uzatmadi <Outlet context={useOutletContext()} />
admin sahifasi qo'lda yozilgan manzil bilan ochildi faqat menyu yashirilgan marshrutda ham RequireRole

9. Mashqlar

Mashqlar kurs/mashqlar/19/07-himoya/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): qayerga tushadi?

keyingiManzil (Hujumchi nigohi bo'limidagi) qaytaradigan qiymatni yozing:

  1. keyingiManzil("/statistika") →
  2. keyingiManzil("statistika") →
  3. keyingiManzil("//example.com") →
  4. keyingiManzil("/vazifa/2?x=1") →
Yechim

1 va 4 — / bilan boshlanadi, keyingi belgi / yoki \ emas: o'zi qaytadi (so'rov qatori ham). 2 — / siz nisbiy yo'l: rad, /. Nisbiy yo'l joriy manzilga qo'shilib, kutilmagan sahifaga olib borishi mumkin edi. 3 — protokolga nisbiy manzil, boshqa domen: rad.

2-mashq (o'rta): qorovulga ?keyin=

Quyidagi qorovul kirmagan foydalanuvchini /kirish ga yuboradi, lekin qaytish manzilini saqlamaydi. Kirgandan keyin har doim / ga tushiladi. Tuzating:

  1. Qorovul ?keyin= ga joriy pathname ni yozsin (URLSearchParams bilan).
  2. Login kirgandan keyin ?keyin= dagi manzilga o'tsin — safeNext funksiyasi orqali (u tayyor, keyingiManzil ning qisqa nusxasi).

Ishora: «?keyin= — qaytish manzili» bo'limi.

jsx
import { createContext, use, useState } from "react";
import {
  createMemoryRouter,
  Navigate,
  Outlet,
  RouterProvider,
  useLocation,
  useNavigate,
  useSearchParams,
} from "react-router";

const AuthContext = createContext(null);

const safeNext = (v) =>
  v !== null && v.startsWith("/") && !/^\/[/\\]/.test(v) ? v : "/";

function Shell() {
  const { pathname } = useLocation();
  return (
    <div>
      <p>Manzil: {pathname}</p>
      <Outlet />
    </div>
  );
}

function RequireAuth() {
  const { user } = use(AuthContext);
  if (user === null) return <Navigate to="/kirish" replace />;
  return <Outlet />;
}

function Login() {
  const { setUser } = use(AuthContext);
  const navigate = useNavigate();
  return (
    <button
      onClick={() => {
        setUser("Bobur");
        navigate("/", { replace: true });
      }}
    >
      Kirish
    </button>
  );
}

const router = createMemoryRouter(
  [
    {
      element: <Shell />,
      children: [
        { path: "/kirish", element: <Login /> },
        {
          element: <RequireAuth />,
          children: [
            { path: "/", element: <p>Bosh sahifa</p> },
            { path: "/savat", element: <p>Savat: 2 ta osh</p> },
          ],
        },
      ],
    },
  ],
  { initialEntries: ["/savat"] },
);

export default function App() {
  const [user, setUser] = useState(null);
  return (
    <AuthContext value={{ user, setUser }}>
      <RouterProvider router={router} />
    </AuthContext>
  );
}

Ekranda:

text
Manzil: /
Bosh sahifa
Yechim
jsx
import { createContext, use, useState } from "react";
import {
  createMemoryRouter,
  Navigate,
  Outlet,
  RouterProvider,
  useLocation,
  useNavigate,
  useSearchParams,
} from "react-router";

const AuthContext = createContext(null);

const safeNext = (v) =>
  v !== null && v.startsWith("/") && !/^\/[/\\]/.test(v) ? v : "/";

function Shell() {
  const { pathname } = useLocation();
  return (
    <div>
      <p>Manzil: {pathname}</p>
      <Outlet />
    </div>
  );
}

function RequireAuth() {
  const { user } = use(AuthContext);
  const { pathname } = useLocation();
  if (user === null) {
    const back = new URLSearchParams({ keyin: pathname });
    return <Navigate to={`/kirish?${back}`} replace />;
  }
  return <Outlet />;
}

function Login() {
  const { setUser } = use(AuthContext);
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();
  return (
    <button
      onClick={() => {
        setUser("Bobur");
        navigate(safeNext(searchParams.get("keyin")), {
          replace: true,
        });
      }}
    >
      Kirish
    </button>
  );
}

const router = createMemoryRouter(
  [
    {
      element: <Shell />,
      children: [
        { path: "/kirish", element: <Login /> },
        {
          element: <RequireAuth />,
          children: [
            { path: "/", element: <p>Bosh sahifa</p> },
            { path: "/savat", element: <p>Savat: 2 ta osh</p> },
          ],
        },
      ],
    },
  ],
  { initialEntries: ["/savat"] },
);

export default function App() {
  const [user, setUser] = useState(null);
  return (
    <AuthContext value={{ user, setUser }}>
      <RouterProvider router={router} />
    </AuthContext>
  );
}

Ekranda:

text
Manzil: /savat
Savat: 2 ta osh

Ilova /savat dan boshlandi, qorovul /kirish?keyin=%2Fsavat ga yubordi (Shell faqat pathname ni yozadi, shuning uchun oraliq ekranda Manzil: /kirish). Kirgandan keyin — savat. safeNext siz searchParams.get("keyin") ni to'g'ridan berish — «Hujumchi nigohi» dagi teshik.

3-mashq (qiyin): admin sahifasi

«Bahor» ga ikki foydalanuvchi va ikki sahifa:

  1. USERS: Malika — ["kassir"], Jasur aka — ["kassir", "admin"]. Ikki tugma ular sifatida "kiradi".
  2. /kassa — kassir uchun, /hisobot — admin uchun. Ruxsat bo'lmasa — <p>403: ruxsat yo'q</p>.
  3. Menyuda «Hisobot» havolasi faqat admin'ga.

Ishora: «Rol — qo'shimcha shart» bo'limidagi can va RequireRole. Boshlang'ich kodda faqat ma'lumot.

jsx
import { createContext, use, useState } from "react";
import {
  createMemoryRouter,
  Link,
  Navigate,
  Outlet,
  RouterProvider,
} from "react-router";

const AuthContext = createContext(null);

const USERS = {
  malika: { name: "Malika", roles: ["kassir"] },
  jasur: { name: "Jasur aka", roles: ["kassir", "admin"] },
};

// can, RequireRole, Shell, router, App — shu yerga
Yechim
jsx
import { createContext, use, useState } from "react";
import {
  createMemoryRouter,
  Link,
  Navigate,
  Outlet,
  RouterProvider,
} from "react-router";

const AuthContext = createContext(null);

const USERS = {
  malika: { name: "Malika", roles: ["kassir"] },
  jasur: { name: "Jasur aka", roles: ["kassir", "admin"] },
};

const can = (user, role) => user?.roles.includes(role) ?? false;

function RequireRole({ role }) {
  const { user } = use(AuthContext);
  if (user === null) return <Navigate to="/" replace />;
  if (!can(user, role)) return <p>403: ruxsat yo'q</p>;
  return <Outlet />;
}

function Shell() {
  const { user, setUser } = use(AuthContext);
  return (
    <div>
      <button onClick={() => setUser(USERS.malika)}>
        Malika
      </button>{" "}
      <button onClick={() => setUser(USERS.jasur)}>Jasur aka</button>
      <p>Foydalanuvchi: {user?.name ?? "mehmon"}</p>
      <nav>
        <Link to="/kassa">Kassa</Link>
        {can(user, "admin") && <Link to="/hisobot"> Hisobot</Link>}
      </nav>
      <Outlet />
    </div>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    children: [
      {
        element: <RequireRole role="kassir" />,
        children: [{ path: "kassa", element: <p>Kassa: 12 chek</p> }],
      },
      {
        element: <RequireRole role="admin" />,
        children: [
          {
            path: "hisobot",
            element: <p>Hisobot: 1 250 000 so'm</p>,
          },
        ],
      },
    ],
  },
]);

export default function App() {
  const [user, setUser] = useState(null);
  return (
    <AuthContext value={{ user, setUser }}>
      <RouterProvider router={router} />
    </AuthContext>
  );
}

Ekranda:

text
Malika Jasur aka
Foydalanuvchi: Jasur aka
Kassa Hisobot
Hisobot: 1 250 000 so'm

Malika kassani ko'rdi, menyuda «Hisobot» yo'q edi. Jasur aka tanlangach havola paydo bo'ldi va hisobot ochildi. Sinab ko'ring: initialEntries: ["/hisobot"] qo'shib, Malika tugmasini bosing — "403: ruxsat yo'q". Menyuda yashirilgan sahifa qo'lda yozilgan manzil bilan ham ochilmaydi. can — bitta joyda: "admin hamma narsani qila oladi" qoidasini qo'shish uchun faqat uni o'zgartirasiz.

4-mashq: Vazifalar qadami

feat/himoyalangan-marshrutlar branch'ida «Vazifalar qadami» bo'limidagi oltita o'zgarishni qiling, keyingiManzil ga testlar (olti yomon qiymat) yozing. Keyin commit.

Yechim

Tekshiruv ro'yxati (npm run dev):

  • «Chiqish» → manzil /kirish, bo'limlar menyusi yo'q, fokus ism maydonida.
  • Chiqqan holda /statistika?x=1 ni oching → /kirish?keyin=%2Fstatistika%3Fx%3D1. Ism yozib kiring → /statistika?x=1. «Orqaga» kirish sahifasiga qaytarmaydi.
  • Kirgan holda /kirish → ro'yxat.
  • Chiqqan holda /vazifa/1 → DevTools Network'da /vazifalar ga so'rov yo'q.
  • /kirish?keyin=//example.com → kirgandan keyin ilovada, / da.
  • Ikkita tab: birida «Chiqish» — ikkinchisi ham /kirish?keyin=… ga o'tadi.
  • npm test — node 183, Vitest 56.

10. Real ishda

  • Har SPA'da — ProtectedRoute/RequireAuth va ?redirect= deyarli har loyihada bor. Intervyuda "login'dan keyin foydalanuvchini qanday qaytarasiz va bu qanday xavfli bo'lishi mumkin?" degan savol ko'p beriladi.
  • Admin panellar — rollar va ruxsatlar (can("buyurtma:ochir")), menyu va tugmalar ruxsatga qarab. CASL kabi kutubxonalar shu qoidalarni bir joyda saqlaydi.
  • Next.js — himoya middleware.ts da yoki server komponentda, serverda (Auth.js darsida). Bugungi uch qatlam (menyu, marshrut, server) o'sha yerda ham.
  • Xavfsizlik auditlari — open redirect OWASP ro'yxatidagi klassik zaiflik; bug bounty dasturlarida tez-tez topiladi.

Xulosa

  • Himoyalangan sahifalar — yo'lsiz layout (element: <RequireAuth />, path yo'q) ichida; yangi sahifa — bitta qator.
  • Kirmagan — <Navigate to="/kirish?keyin=…" replace />; kirgach — ?keyin= dagi manzilga, replace bilan.
  • ?keyin= — foydalanuvchi kiritgan ma'lumot: faqat / bilan boshlanadigan, // va /\ siz ichki yo'l (ochiq yo'naltirish).
  • Miltillashga qarshi — uch holat (tekshirilmoqda/mehmon/kirgan); bo'sh kadrga qarshi — <Navigate> bilan birga forma.
  • Rollar: menyu (qulaylik) + marshrut (403) + server (haqiqiy himoya).
  • Loader qorovuldan oldin ishlaydi — u o'zi throw replace(…) qiladi (redirect() emas: yangi tabda «Orqaga» yana /kirish ga tushardi — o'lchandi). Middleware o'lchandi: ro'yxatning birinchi kadrini kechiktiradi — kanonga kirmadi.

Keyingi dars: TanStack Router: to'liq type-safe routing — React Router'ga muqobil: yo'llar va parametrlar kompilyatsiya paytida tekshiriladi. vazifalar da o'lchab solishtiramiz.

Manbalar

  • React Router: Navigate, useSearchParams, redirect — reactrouter.com/api/… (2026-10 holatiga, v8.4.0)
  • React Router: Middleware — reactrouter.com/how-to/middleware
  • OWASP: Unvalidated Redirects and Forwards Cheat Sheet — cheatsheetseries.owasp.org
  • MDN: URLSearchParams — developer.mozilla.org
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React Router himoyalangan marshrutlar: Navigate, ?keyin= qaytish, rollar va loader'da tekshirish — IlmHamroh