"use client"; /** * MIRROR of main frontend `frontend/src/components/auth/RouteGuard.tsx` — * keep in sync manually. * * Особенность tradein-mvp: Next.js basePath=/trade-in (см. `next.config.ts`). * `usePathname()` возвращает путь БЕЗ basePath — например на странице * `gendsgn.ru/trade-in/scrapers/avito` хук вернёт `/scrapers/avito`. * RBAC config (`auth/roles.yaml`) использует абсолютные пути сайта * (`/trade-in/**`, `/trade-in/api/v1/admin/**`), поэтому перед проверкой * isPathAllowed мы префиксим pathname через NEXT_PUBLIC_BASE_PATH. */ import { usePathname } from "next/navigation"; import { NoAccessScreen } from "@/components/auth/NoAccessScreen"; import { HTTPError } from "@/lib/api"; import { isPathAllowed } from "@/lib/isPathAllowed"; import { useMe } from "@/lib/useMe"; const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; // #801: dev/CI-only preview-маршрут (a11y/lighthouse) рендерится оффлайн без RBAC. const ENABLE_PREVIEW = process.env.NEXT_PUBLIC_ENABLE_PREVIEW === "1"; interface RouteGuardProps { children: React.ReactNode; } export function RouteGuard({ children }: RouteGuardProps) { const rawPath = usePathname() ?? "/"; // Абсолютный путь сайта: BASE_PATH + rawPath. Аккуратно с двойным слэшем // на `/`: `BASE_PATH = "/trade-in"` + `"/"` → `/trade-in/` (ок). const absolutePath = BASE_PATH ? `${BASE_PATH}${rawPath === "/" ? "/" : rawPath}` : rawPath; const { data, isLoading, error } = useMe(); // #801: preview-страница самодостаточна (свой QueryClient с фейковым me), // RBAC к ней не применяем. Только под флагом — в проде по умолчанию выключено. if (ENABLE_PREVIEW && rawPath.startsWith("/ui-preview")) { return <>{children}; } if (isLoading) return null; if (error instanceof HTTPError && error.status === 401) { // Dev without Caddy: 401 is normal, mount the app so local dev works. // Prod: mounting children on 401 causes TanStack Query re-subscribe storm // (each new observer on errored query triggers a refetch). Show session screen // instead — prevents the subtree from mounting, kills the loop. if (process.env.NODE_ENV !== "production") return <>{children}; return ; } if (error instanceof HTTPError && error.status === 403) { return ; } if (error) { return ; } if (!data) return null; // Пробный доступ закончился (#praktika): role=expired → спец-экран, а не generic path-deny. if (data.role === "expired") { return ; } if (!isPathAllowed(data.allowed_paths, data.deny_paths, absolutePath)) { return ; } return <>{children}; }