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
PR B (frontend) to backend RBAC #585. RouteGuard wraps app, NoAccessScreen on 403 from /me, TopNav filtered by role.
107 lines
3.8 KiB
TypeScript
107 lines
3.8 KiB
TypeScript
"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>
|
||
);
|
||
}
|