gendesign/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx
bot-backend 703e411a17
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(tradein/auth): expire praktika trial access + trial-ended screen (contact Artem)
praktika pilot→expired role in roles.yaml (paths:[], deny:/**); RouteGuard
short-circuits on role=expired with a dedicated trial-ended screen (not the
generic path-deny path). NoAccessScreen gains variant="trial" with title
«Пробный доступ закончился» and a Telegram link to @ArtemKopylov87. Backend
Role Literal and frontend Role type extended to include "expired". kopylov
(pilot) unaffected.
2026-06-27 13:30:28 +03:00

76 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 <NoAccessScreen variant="session" />;
}
if (error instanceof HTTPError && error.status === 403) {
return <NoAccessScreen variant="user" />;
}
if (error) {
return <NoAccessScreen variant="user" />;
}
if (!data) return null;
// Пробный доступ закончился (#praktika): role=expired → спец-экран, а не generic path-deny.
if (data.role === "expired") {
return <NoAccessScreen variant="trial" />;
}
if (!isPathAllowed(data.allowed_paths, data.deny_paths, absolutePath)) {
return <NoAccessScreen variant="path" path={absolutePath} />;
}
return <>{children}</>;
}