"use client";
/**
* RouteGuard — клиентский гард, оборачивающий весь app в layout.tsx.
*
* Логика:
* - useMe() loading → null (короткое окно; основной UI вернётся когда
* /me ответит, обычно <50 ms из браузер-cache после первого захода)
* - useMe() 403 (юзер не в roles.yaml) → fullscreen NoAccessScreen("user")
* - useMe() 401 (dev без Caddy basic_auth) → пропускаем (dev escape-hatch).
* Это критично — без этого локально без Caddy фронт станет полностью
* белым. В проде Caddy всегда ставит X-Authenticated-User → 401 невозможен.
* - useMe() ok + pathname ∉ allowed_paths (или ∈ deny_paths) → fullscreen
* NoAccessScreen("path"). Не редиректим — у user намеренный direct-URL
* hit на /admin (например), и redirect замаскирует, что у него нет
* доступа. Прямо говорим «нет».
* - useMe() ok + pathname разрешён → пропускаем children.
*
* Backend всё равно сам блокирует /api/v1/* — этот гард только для UX
* («не показывать ссылки и не позволять открыть страницу через URL»).
*/
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";
interface RouteGuardProps {
children: React.ReactNode;
}
export function RouteGuard({ children }: RouteGuardProps) {
const pathname = usePathname() ?? "/";
const { data, isLoading, error } = useMe();
// Initial load — короткое окно, рендерим пустоту чтобы не моргать UI.
if (isLoading) return null;
// 401 — dev без Caddy basic_auth. Пропускаем как «нет RBAC, all allowed».
if (error instanceof HTTPError && error.status === 401) {
return <>{children}>;
}
// 403 — юзер не в roles.yaml. Прям ничего не показываем кроме «доступа нет».
if (error instanceof HTTPError && error.status === 403) {
return ;
}
// Другая сетевая ошибка — fail-open в dev / показываем NoAccess в prod-like.
// Безопаснее закрыть UI чем случайно пустить юзера на admin при transient
// network fail. NoAccessScreen("user") универсальный fallback.
if (error) {
return ;
}
// Нет scope (теоретически unreachable — либо error либо data) — заглушка.
if (!data) return null;
// Path-level guard на прямой URL. Backend всё равно 403 на /api/v1/admin/*,
// но мы не хотим показать «битый admin UI» — лучше сразу stop.
if (!isPathAllowed(data.allowed_paths, data.deny_paths, pathname)) {
// #799: pilot-only юзер (единственная доступная зона — trade-in) на
// запрещённом пути главного фронта (корень `/` и пр.) → авто-redirect в
// /trade-in/ вместо дед-энда «Доступа нет». Условия:
// - роль pilot (trade-in — её ЕДИНСТВЕННАЯ рабочая зона; paths scoped
// только на /trade-in/**). #1439: без этой проверки multi-zone роли
// (напр. analyst с paths="/**") на запрещённом /admin/* молча
// редиректились в /trade-in/ вместо честного NoAccessScreen;
// - текущий путь НЕ под /trade-in (иначе цикл; /trade-in — отдельный
// bundle за Caddy, этот гард его не обслуживает, но guard на всякий);
// - у роли есть доступ к /trade-in/ (т.е. trade-in — её рабочая зона).
// admin / multi-scope с разрешённым `/` сюда не попадают (isPathAllowed=true).
if (
typeof window !== "undefined" &&
data.role === "pilot" &&
!pathname.startsWith("/trade-in") &&
isPathAllowed(data.allowed_paths, data.deny_paths, "/trade-in/")
) {
window.location.replace("/trade-in/");
return null;
}
return ;
}
return <>{children}>;
}