Files
pwa/src/components/ProtectedRoute.tsx
T

62 lines
2.2 KiB
TypeScript
Raw Normal View History

import { useMemo } from 'react';
2025-11-18 20:22:25 +03:30
import { Navigate, useLocation } from 'react-router-dom';
import { useAtom } from 'jotai';
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;
/** Allow access while name/age are still missing (profile completion page). */
allowIncompleteProfile?: boolean;
2025-11-18 20:22:25 +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);
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();
// 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
if (!authInitialCheckDone) {
return <AuthBootSpinner />;
2025-11-18 20:22:25 +03:30
}
if (!guestSessionHydrated) {
return <AuthBootSpinner />;
2025-11-18 20:22:25 +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
}