"use client"; /* eslint-disable @next/next/no-img-element -- white-label логотип хотлинком с сайта клиента, next/image не нужен */ import { UserMenu } from "@/components/auth/UserMenu"; import { API_BASE_URL, HTTPError } from "@/lib/api"; import { isPathAllowed } from "@/lib/isPathAllowed"; import { useBrand } from "@/lib/useBrand"; 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(); // #657 white-label: при активном ?brand= с логотипом рендерим логотип // клиента вместо «TI»-метки. Без бренда brand === null → стандартный wordmark. const { data: brand } = useBrand(); // 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 (
{brand?.logo_url ? ( <> {brand.name} {brand.name} ) : ( <> TI Trade-In )}
); }