2026-09-03 21:01:52 +03:30
|
|
|
import { useMemo } from 'react';
|
2025-11-18 20:22:25 +03:30
|
|
|
import { Navigate, useLocation } from 'react-router-dom';
|
|
|
|
|
import { useAtom } from 'jotai';
|
2026-09-03 21:01:52 +03:30
|
|
|
import {
|
|
|
|
|
backendUserAtom,
|
|
|
|
|
isAuthenticatedAtom,
|
|
|
|
|
authInitialCheckDoneAtom,
|
|
|
|
|
guestSessionAtom,
|
|
|
|
|
guestSessionHydratedAtom,
|
|
|
|
|
} from '@/atoms/auth';
|
|
|
|
|
import { hasSessionToken } from '@/services/backend';
|
|
|
|
|
import { ROUTES } from '@/utils/constants';
|
2025-11-18 20:22:25 +03:30
|
|
|
|
|
|
|
|
interface ProtectedRouteProps {
|
|
|
|
|
children: React.ReactNode;
|
2026-09-03 21:01:52 +03:30
|
|
|
/** Allow access while name/age are still missing (profile completion page). */
|
|
|
|
|
allowIncompleteProfile?: boolean;
|
2025-11-18 20:22:25 +03:30
|
|
|
}
|
|
|
|
|
|
2026-09-03 21:01:52 +03:30
|
|
|
const AuthBootSpinner = () => (
|
|
|
|
|
<div className="flex h-full min-h-0 flex-1 items-center justify-center">
|
|
|
|
|
<div className="h-12 w-12 animate-spin rounded-full border-2 border-neon-cyan/30 border-t-neon-cyan" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export function ProtectedRoute({ children, allowIncompleteProfile = false }: ProtectedRouteProps) {
|
2025-11-18 20:22:25 +03:30
|
|
|
const [isAuthenticated] = useAtom(isAuthenticatedAtom);
|
2026-09-03 21:01:52 +03:30
|
|
|
const [backendUser] = useAtom(backendUserAtom);
|
|
|
|
|
const [authInitialCheckDone] = useAtom(authInitialCheckDoneAtom);
|
|
|
|
|
const [guestSession] = useAtom(guestSessionAtom);
|
|
|
|
|
const [guestSessionHydrated] = useAtom(guestSessionHydratedAtom);
|
2025-11-18 20:22:25 +03:30
|
|
|
const location = useLocation();
|
2026-09-03 21:01:52 +03:30
|
|
|
// Stabilize Navigate `state` — a new object every render re-triggers Navigate's
|
|
|
|
|
// useEffect (deps include `state`) and can blow the update depth limit.
|
|
|
|
|
const loginRedirectState = useMemo(
|
|
|
|
|
() => ({ from: { pathname: location.pathname } }),
|
|
|
|
|
[location.pathname],
|
|
|
|
|
);
|
|
|
|
|
const hasToken = hasSessionToken();
|
|
|
|
|
const canUseBackendUser = Boolean(backendUser && hasToken);
|
2025-11-18 20:22:25 +03:30
|
|
|
|
2026-09-03 21:01:52 +03:30
|
|
|
if (!authInitialCheckDone) {
|
|
|
|
|
return <AuthBootSpinner />;
|
2025-11-18 20:22:25 +03:30
|
|
|
}
|
|
|
|
|
|
2026-09-03 21:01:52 +03:30
|
|
|
if (!guestSessionHydrated) {
|
|
|
|
|
return <AuthBootSpinner />;
|
2025-11-18 20:22:25 +03:30
|
|
|
}
|
|
|
|
|
|
2026-09-03 21:01:52 +03:30
|
|
|
if (!isAuthenticated && !guestSession) {
|
|
|
|
|
return <Navigate to={ROUTES.LOGIN} state={loginRedirectState} replace />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Incomplete-profile gate only when a live session exists. Stale backendUser
|
|
|
|
|
// without a token must not bounce / ↔ /profile forever.
|
|
|
|
|
if (!allowIncompleteProfile && canUseBackendUser && !backendUser!.profileComplete) {
|
|
|
|
|
return <Navigate to={ROUTES.PROFILE} replace />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <div className="flex h-full min-h-0 flex-1 flex-col overflow-hidden">{children}</div>;
|
2025-11-18 20:22:25 +03:30
|
|
|
}
|