diff --git a/src/components/AuthGate.tsx b/src/components/AuthGate.tsx index a227a74..18ecb56 100644 --- a/src/components/AuthGate.tsx +++ b/src/components/AuthGate.tsx @@ -18,16 +18,44 @@ export function AuthGate({ children }: Props) { const [checking, setChecking] = useState(true) const navigate = useNavigate() const location = useLocation() + const userRef = useRef(null) - useEffect(() => { + function fetchMe(isInitial = false) { fetch('/api/auth/me', { credentials: 'include' }) .then(r => r.ok ? r.json() : null) .then(data => { - if (data) setUser(data) - else navigate('/login', { replace: true, state: { from: location.pathname } }) + if (data) { + // Only update state if the user identity has changed, to avoid re-renders on every focus + if (!userRef.current || userRef.current.id !== data.id) { + userRef.current = data + setUser(data) + } + } else { + navigate('/login', { replace: true, state: isInitial ? { from: location.pathname } : undefined }) + } }) .catch(() => navigate('/login', { replace: true })) - .finally(() => setChecking(false)) + .finally(() => { if (isInitial) setChecking(false) }) + } + + useEffect(() => { + fetchMe(true) + + // Re-check session on tab focus / visibility — catches the case where a + // different user logged into a child app and the portal cookie changed. + let pending = false + function onVisible() { + if (document.visibilityState !== 'visible' || pending) return + pending = true + setTimeout(() => { pending = false; fetchMe() }, 100) + } + + document.addEventListener('visibilitychange', onVisible) + window.addEventListener('focus', onVisible) + return () => { + document.removeEventListener('visibilitychange', onVisible) + window.removeEventListener('focus', onVisible) + } }, []) if (checking) {