"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}>;
}