fix(super-admin): убрать цикл редиректа, регресс override после пакета задач
Some checks are pending
Some checks are pending
КОРНЕВАЯ ПРИЧИНА: 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) <noreply@anthropic.com>
This commit is contained in:
parent
18a0370d96
commit
4152eb1291
|
|
@ -1,12 +1,12 @@
|
||||||
import { useEffect, useState } from 'react'
|
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 { useQuery } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
ShieldCheck, Building, FileClock, HeartPulse, HardDriveDownload,
|
ShieldCheck, Building, FileClock, HeartPulse, HardDriveDownload,
|
||||||
Settings, Users, LayoutDashboard, LogOut, Menu, X, ChevronDown, Globe,
|
Settings, Users, LayoutDashboard, LogOut, Menu, X, ChevronDown, Globe,
|
||||||
FolderTree, Ruler,
|
FolderTree, Ruler,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { api, getOrgOverride, setOrgOverride } from '@/lib/api'
|
import { api, setOrgOverride } from '@/lib/api'
|
||||||
import { logout } from '@/lib/auth'
|
import { logout } from '@/lib/auth'
|
||||||
import { useMe } from '@/lib/useMe'
|
import { useMe } from '@/lib/useMe'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
@ -40,7 +40,6 @@ interface OrgRow { id: string; name: string; isArchived: boolean }
|
||||||
|
|
||||||
export function SuperAdminLayout() {
|
export function SuperAdminLayout() {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||||
const [orgPickerOpen, setOrgPickerOpen] = useState(false)
|
const [orgPickerOpen, setOrgPickerOpen] = useState(false)
|
||||||
|
|
@ -55,11 +54,12 @@ export function SuperAdminLayout() {
|
||||||
return () => { document.title = prev }
|
return () => { document.title = prev }
|
||||||
}, [location.pathname])
|
}, [location.pathname])
|
||||||
|
|
||||||
// Если у юзера активен override — это режим «открыто как…», SuperAdmin
|
// Раньше тут стоял useEffect navigate('/dashboard') если getOrgOverride()
|
||||||
// должен видеть tenant-layout, а не системную консоль. Перебрасываем.
|
// не пуст — это создавало цикл: SuperAdmin не мог зайти в системную консоль
|
||||||
useEffect(() => {
|
// когда override остался с прошлой сессии (его выкидывало обратно в
|
||||||
if (getOrgOverride()) navigate('/dashboard', { replace: true })
|
// tenant-режим, баннер с «Выйти из режима» тоже в tenant-области).
|
||||||
}, [navigate, location.pathname])
|
// Теперь переход в системную консоль безопасен — override остаётся, юзер
|
||||||
|
// его сам снимает через баннер либо переключает другую орг через picker.
|
||||||
|
|
||||||
// Закрывать drawer при смене маршрута.
|
// Закрывать drawer при смене маршрута.
|
||||||
useEffect(() => { setDrawerOpen(false) }, [location.pathname])
|
useEffect(() => { setDrawerOpen(false) }, [location.pathname])
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,36 @@
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useMe } from '@/lib/useMe'
|
import { useMe } from '@/lib/useMe'
|
||||||
import { getOrgOverride } from '@/lib/api'
|
import { getOrgOverride } from '@/lib/api'
|
||||||
|
|
||||||
/** Не пускает SuperAdmin'а в tenant-роуты без активного override —
|
/** Не пускает SuperAdmin'а в tenant-роуты без активного override.
|
||||||
* SuperAdmin сам по себе не сотрудник конкретной орги, любая tenant-страница
|
* Защита от race: между window.location.assign('/dashboard') и тем
|
||||||
* для него бессмысленна. Если override не активен — редирект на
|
* моментом когда сюда докатилось me.data — localStorage уже точно
|
||||||
* /super-admin/organizations с alert'ом «Откройте организацию через "Открыть как…"». */
|
* установлен. На всякий случай делаем повторную проверку через rAF
|
||||||
|
* перед редиректом — раньше были репорты что после Phase 2c сценарий
|
||||||
|
* клика «Открыть как» снова приводит к alert'у, оказалось это был
|
||||||
|
* цикл редиректа в SuperAdminLayout (убран отдельным фиксом). */
|
||||||
export function TenantRouteGuard({ children }: { children: React.ReactNode }) {
|
export function TenantRouteGuard({ children }: { children: React.ReactNode }) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const checkedOnceRef = useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!me.data) return
|
if (!me.data) return
|
||||||
const isSuper = me.data.roles?.includes('SuperAdmin')
|
const isSuper = me.data.roles?.includes('SuperAdmin')
|
||||||
if (isSuper && !getOrgOverride()) {
|
if (!isSuper) return
|
||||||
// Маленькое уведомление перед редиректом — alert удобнее тоста для разовой ситуации.
|
// 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 */ }
|
try { sessionStorage.setItem('tenant-guard-msg', 'Откройте конкретную организацию через "Открыть как…"') } catch { /* ignore */ }
|
||||||
navigate('/super-admin/organizations', { replace: true })
|
navigate('/super-admin/organizations', { replace: true })
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}, [me.data, navigate])
|
}, [me.data, navigate])
|
||||||
return <>{children}</>
|
return <>{children}</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue