// ============================================================ // ZCLAW Admin V2 — Auth Guard with session restore // ============================================================ // // Auth strategy: // 1. On first mount, always validate the HttpOnly cookie via GET /auth/me // 2. If cookie valid -> restore session and render children // 3. If cookie invalid -> clean up and redirect to /login // 4. If already authenticated (from login flow) -> render immediately // // This eliminates the race condition where localStorage had account data // but the HttpOnly cookie was expired, causing children to render and // make failing API calls. import { useEffect, useRef, useState } from 'react' import { Navigate, useLocation } from 'react-router-dom' import { Spin } from 'antd' import { useAuthStore } from '@/stores/authStore' import { authService } from '@/services/auth' type GuardState = 'checking' | 'authenticated' | 'unauthenticated' export function AuthGuard({ children }: { children: React.ReactNode }) { const isAuthenticated = useAuthStore((s) => s.isAuthenticated) const login = useAuthStore((s) => s.login) const logout = useAuthStore((s) => s.logout) const location = useLocation() // Track validation attempt to avoid double-calling (React StrictMode) const validated = useRef(false) const [guardState, setGuardState] = useState( isAuthenticated ? 'authenticated' : 'checking' ) useEffect(() => { // Already authenticated from login flow — skip validation if (isAuthenticated) { setGuardState('authenticated') return } // Prevent double-validation in React StrictMode if (validated.current) return validated.current = true // Validate HttpOnly cookie via /auth/me authService.me() .then((meAccount) => { login(meAccount) setGuardState('authenticated') }) .catch(() => { logout() setGuardState('unauthenticated') }) }, []) // eslint-disable-line react-hooks/exhaustive-deps if (guardState === 'checking') { return (
) } if (guardState === 'unauthenticated') { return } return <>{children} }