From 4152eb1291d03ce87bbe456f794aea3157e4dbb1 Mon Sep 17 00:00:00 2001 From: nns <278048682+nurdotnet@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:54:37 +0500 Subject: [PATCH] =?UTF-8?q?fix(super-admin):=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D1=86=D0=B8=D0=BA=D0=BB=20=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B8=D1=80=D0=B5=D0=BA=D1=82=D0=B0,=20=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=81=20override=20=D0=BF=D0=BE=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=B0=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D1=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit КОРНЕВАЯ ПРИЧИНА: SuperAdminLayout имел useEffect if (getOrgOverride()) navigate('/dashboard', { replace: true }) который автоматически выкидывал в tenant-режим при любом заходе на /super-admin/* если в localStorage остался override (например с прошлой сессии или после нечистого выхода). Это создавало цикл: SuperAdmin кликает «Системная консоль» в меню → попадает на /super-admin/... → useEffect видит override → navigate /dashboard → tenant TenantRouteGuard проверяет: override есть → пропускает, но юзер видит tenant с баннером вместо системной консоли. Если override содержал «зависший» orgId с прошлой сессии и юзер ожидал вернуться в SuperAdmin консоль — он попадал в tenant-режим, а попытки вернуться через ссылки sidebar лечились этим же useEffect снова в / dashboard. Юзер видел тост от TenantRouteGuard в редких случаях когда localStorage не успевал прочитаться (race с rAF). Фикс: - Удалён useEffect (+ unused useNavigate импорт). SuperAdmin может свободно перемещаться между системной консолью и tenant-режимом при активном override; снимает override только баннером «Выйти из режима» либо переключает другую орг через picker. - TenantRouteGuard: повторная проверка getOrgOverride() через requestAnimationFrame (защита от любых race с localStorage), и разовый guard-флаг (useRef) чтобы не редиректить дважды на одной странице. Console.warn перед редиректом — облегчает диагностику если регресс повторится. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/SuperAdminLayout.tsx | 16 ++++----- .../src/components/TenantRouteGuard.tsx | 33 +++++++++++++------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/food-market.web/src/components/SuperAdminLayout.tsx b/src/food-market.web/src/components/SuperAdminLayout.tsx index f2e0b73..d011381 100644 --- a/src/food-market.web/src/components/SuperAdminLayout.tsx +++ b/src/food-market.web/src/components/SuperAdminLayout.tsx @@ -1,12 +1,12 @@ import { useEffect, useState } from 'react' -import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom' +import { NavLink, Outlet, useLocation } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { ShieldCheck, Building, FileClock, HeartPulse, HardDriveDownload, Settings, Users, LayoutDashboard, LogOut, Menu, X, ChevronDown, Globe, FolderTree, Ruler, } from 'lucide-react' -import { api, getOrgOverride, setOrgOverride } from '@/lib/api' +import { api, setOrgOverride } from '@/lib/api' import { logout } from '@/lib/auth' import { useMe } from '@/lib/useMe' import { cn } from '@/lib/utils' @@ -40,7 +40,6 @@ interface OrgRow { id: string; name: string; isArchived: boolean } export function SuperAdminLayout() { const me = useMe() - const navigate = useNavigate() const location = useLocation() const [drawerOpen, setDrawerOpen] = useState(false) const [orgPickerOpen, setOrgPickerOpen] = useState(false) @@ -55,11 +54,12 @@ export function SuperAdminLayout() { return () => { document.title = prev } }, [location.pathname]) - // Если у юзера активен override — это режим «открыто как…», SuperAdmin - // должен видеть tenant-layout, а не системную консоль. Перебрасываем. - useEffect(() => { - if (getOrgOverride()) navigate('/dashboard', { replace: true }) - }, [navigate, location.pathname]) + // Раньше тут стоял useEffect navigate('/dashboard') если getOrgOverride() + // не пуст — это создавало цикл: SuperAdmin не мог зайти в системную консоль + // когда override остался с прошлой сессии (его выкидывало обратно в + // tenant-режим, баннер с «Выйти из режима» тоже в tenant-области). + // Теперь переход в системную консоль безопасен — override остаётся, юзер + // его сам снимает через баннер либо переключает другую орг через picker. // Закрывать drawer при смене маршрута. useEffect(() => { setDrawerOpen(false) }, [location.pathname]) diff --git a/src/food-market.web/src/components/TenantRouteGuard.tsx b/src/food-market.web/src/components/TenantRouteGuard.tsx index bd66079..9a775c3 100644 --- a/src/food-market.web/src/components/TenantRouteGuard.tsx +++ b/src/food-market.web/src/components/TenantRouteGuard.tsx @@ -1,23 +1,36 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { useMe } from '@/lib/useMe' import { getOrgOverride } from '@/lib/api' -/** Не пускает SuperAdmin'а в tenant-роуты без активного override — - * SuperAdmin сам по себе не сотрудник конкретной орги, любая tenant-страница - * для него бессмысленна. Если override не активен — редирект на - * /super-admin/organizations с alert'ом «Откройте организацию через "Открыть как…"». */ +/** Не пускает SuperAdmin'а в tenant-роуты без активного override. + * Защита от race: между window.location.assign('/dashboard') и тем + * моментом когда сюда докатилось me.data — localStorage уже точно + * установлен. На всякий случай делаем повторную проверку через rAF + * перед редиректом — раньше были репорты что после Phase 2c сценарий + * клика «Открыть как» снова приводит к alert'у, оказалось это был + * цикл редиректа в SuperAdminLayout (убран отдельным фиксом). */ export function TenantRouteGuard({ children }: { children: React.ReactNode }) { const me = useMe() const navigate = useNavigate() + const checkedOnceRef = useRef(false) useEffect(() => { if (!me.data) return const isSuper = me.data.roles?.includes('SuperAdmin') - if (isSuper && !getOrgOverride()) { - // Маленькое уведомление перед редиректом — alert удобнее тоста для разовой ситуации. - try { sessionStorage.setItem('tenant-guard-msg', 'Откройте конкретную организацию через "Открыть как…"') } catch { /* ignore */ } - navigate('/super-admin/organizations', { replace: true }) - } + if (!isSuper) return + // Re-read через rAF чтобы перебить любые гонки с инициализацией localStorage. + requestAnimationFrame(() => { + const ov = getOrgOverride() + if (!ov) { + if (checkedOnceRef.current) return + checkedOnceRef.current = true + // eslint-disable-next-line no-console + console.warn('[TenantRouteGuard] SuperAdmin без override → redirect /super-admin/organizations', + { hasLocalStorage: !!localStorage.getItem('superAdminAsOrg') }) + try { sessionStorage.setItem('tenant-guard-msg', 'Откройте конкретную организацию через "Открыть как…"') } catch { /* ignore */ } + navigate('/super-admin/organizations', { replace: true }) + } + }) }, [me.data, navigate]) return <>{children} }