gendesign/frontend/src/components/auth/RouteGuard.tsx
Light1YT 86e9ea2937 fix(week-review): автофиксы код-ревью — 169 issue (label «week ревью 1»)
Многоагентный аудит + имплементация: один воркер на файл, точечные правки.
Верификация: py_compile (47/47 .py) + tsc --noEmit (0 ошибок). Unit-тесты
не прогонялись (окружение не поднято: rollup native dep / нет pytest-venv).

Полностью исправлено (169): #1336, #1337, #1339, #1340, #1341, #1342, #1343, #1345, #1346, #1348, #1349, #1350, #1351, #1354, #1356, #1358, #1359, #1360, #1362, #1364, #1365, #1366, #1367, #1368, #1369, #1370, #1371, #1372, #1373, #1374, #1375, #1376, #1377, #1378, #1379, #1380, #1381, #1382, #1384, #1385, #1386, #1387, #1388, #1389, #1390, #1391, #1392, #1394, #1395, #1396, #1397, #1399, #1400, #1401, #1402, #1403, #1404, #1408, #1409, #1410, #1411, #1412, #1413, #1414, #1415, #1416, #1417, #1418, #1420, #1423, #1425, #1426, #1427, #1428, #1429, #1430, #1431, #1432, #1433, #1434, #1435, #1437, #1438, #1439, #1440, #1441, #1442, #1443, #1444, #1445, #1446, #1447, #1448, #1449, #1450, #1451, #1452, #1453, #1454, #1455, #1456, #1457, #1458, #1459, #1460, #1461, #1462, #1463, #1464, #1465, #1466, #1467, #1468, #1469, #1471, #1472, #1473, #1474, #1476, #1478, #1479, #1481, #1482, #1483, #1484, #1485, #1487, #1488, #1489, #1490, #1491, #1492, #1493, #1494, #1495, #1496, #1497, #1499, #1500, #1501, #1502, #1504, #1505, #1506, #1507, #1510, #1514, #1515, #1516, #1517, #1518, #1519, #1521, #1522, #1523, #1524, #1525, #1526, #1527, #1528, #1529, #1531, #1532, #1533, #1534, #1535, #1536, #1537, #1538

Частично (9, in-file часть, остаток cross-file): #1361, #1419, #1422, #1424, #1470, #1475, #1477, #1480, #1498
Требуют cross-file (3, не тронуты): #1338, #1363, #1421
Пропущено (1): #1539

Не входило в партию: 22 needs-Leha issue (нужны решения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:21:11 +05:00

88 lines
4.7 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";
/**
* 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 <NoAccessScreen variant="user" />;
}
// Другая сетевая ошибка — fail-open в dev / показываем NoAccess в prod-like.
// Безопаснее закрыть UI чем случайно пустить юзера на admin при transient
// network fail. NoAccessScreen("user") универсальный fallback.
if (error) {
return <NoAccessScreen variant="user" />;
}
// Нет 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 <NoAccessScreen variant="path" path={pathname} />;
}
return <>{children}</>;
}