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