gendesign/tradein-mvp/frontend/src/components/trade-in/Topbar.tsx
Light1YT 7a2b055b35
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 1m42s
Deploy Trade-In / deploy (push) Successful in 39s
Deploy / build-frontend (push) Successful in 2m55s
Deploy / deploy (push) Successful in 1m1s
feat(rbac): frontend route-guard + nav filter (#586)
PR B (frontend) to backend RBAC #585. RouteGuard wraps app, NoAccessScreen on 403 from /me, TopNav filtered by role.
2026-05-26 06:51:45 +00:00

107 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { API_BASE_URL, HTTPError } from "@/lib/api";
import { isPathAllowed } from "@/lib/isPathAllowed";
import { useMe } from "@/lib/useMe";
type ActiveTab =
| "estimate"
| "history"
| "cache"
| "avito"
| "cian"
| "yandex"
| "cian-cookies";
interface TopbarProps {
active: ActiveTab;
}
/**
* Nav items с двумя путями:
* `href` — относительный путь, который рендерится в URL (basePath
* `/trade-in` префиксится через `API_BASE_URL`).
* `scopePath` — абсолютный путь как его видит backend RBAC config
* (`auth/roles.yaml`).
*
* Caveat / UX-opinion: pages вида `/trade-in/scrapers/*` сами по себе НЕ
* в `pilot.deny` (там только `/trade-in/api/v1/admin/**`). По букве yaml
* pilot имеет право видеть и UI скраперов — но это admin-функционал, и
* рендерить «битый» dashboard с 403-ошибками на каждый data-fetch — плохой
* UX. Поэтому для фильтрации Topbar мы маппим scraper-таб на admin-API
* путь, чтобы pilot их не видел в навигации. Direct URL access на
* `/trade-in/scrapers/avito` НЕ блокируется (RouteGuard следует yaml). Если
* нужна полная блокировка — добавить `/trade-in/scrapers/**` в pilot.deny.
*/
const NAV_ITEMS: Array<{
key: ActiveTab;
href: string;
scopePath: string;
label: string;
}> = [
{ key: "estimate", href: "/", scopePath: "/trade-in/", label: "Оценка" },
{ key: "history", href: "/history", scopePath: "/trade-in/history", label: "История" },
{ key: "cache", href: "/cache", scopePath: "/trade-in/cache", label: "Кэш" },
// Скраперы — admin-only UI. Маппим на admin-deny path, чтобы pilot их не видел.
{
key: "avito",
href: "/scrapers/avito",
scopePath: "/trade-in/api/v1/admin/scrapers/avito",
label: "Авито",
},
{
key: "cian",
href: "/scrapers/cian",
scopePath: "/trade-in/api/v1/admin/scrapers/cian",
label: "Циан",
},
{
key: "yandex",
href: "/scrapers/yandex",
scopePath: "/trade-in/api/v1/admin/scrapers/yandex",
label: "Яндекс",
},
{
key: "cian-cookies",
href: "/scrapers/cian-cookies",
scopePath: "/trade-in/api/v1/admin/scrapers/cian-cookies",
label: "Cian Cookies",
},
];
export function Topbar({ active }: TopbarProps) {
const { data, error } = useMe();
// 401 (dev без Caddy basic_auth) — показываем все айтемы.
// Если /me ещё грузится или дал ошибку без scope — fallback на все айтемы
// (RouteGuard в этом случае сам решит, рендерить ли страницу вообще).
const isDev401 = error instanceof HTTPError && error.status === 401;
const items =
data && !isDev401
? NAV_ITEMS.filter((item) =>
isPathAllowed(data.allowed_paths, data.deny_paths, item.scopePath),
)
: NAV_ITEMS;
return (
<header className="topbar">
<div className="topbar-inner">
<div className="brand">
<span className="brand-mark">TI</span>
<span className="brand-product">Trade-In</span>
</div>
<nav className="top-nav">
{items.map((item) => (
<a
key={item.key}
href={`${API_BASE_URL}${item.href}`}
className={item.key === active ? "is-active" : undefined}
>
{item.label}
</a>
))}
</nav>
</div>
</header>
);
}