session-provider.tsx3.8 KBView on GitHub /**
* SessionGuard - Authentication gate without context
*
* Simply checks if user is authenticated and shows loading/redirect states.
* No context provider = no re-renders when session changes.
* Components can use useSession() directly from better-auth for session data.
*
* Benefits:
* - Zero re-renders from session state changes
* - Simple authentication gate at layout level
* - Better-auth handles all session management
* - Components get session data directly when needed
*/
import { useSession } from '@/modules/auth/utils/auth-client';
import { useEffect, useState, type PropsWithChildren } from 'react';
import { Link, Navigate } from 'react-router';
export function SessionGuard({ children }: PropsWithChildren) {
const { data: session, isPending, error } = useSession();
// Loading state - show spinner while checking session
if (isPending) {
return (
<div className="flex h-screen w-screen items-center justify-center">
<div className="text-center">
<div className="border-accent mx-auto mb-2 h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
<div className="text-muted-foreground text-sm">Verifying authentication...</div>
</div>
</div>
);
}
// Distinguish transient errors (network / 5xx) from explicit auth rejection.
// BetterFetchError always has a numeric `status`; treat 0/missing as network
// failure, 5xx as transient backend, and 4xx as a real auth rejection.
if (error) {
const status = error.status ?? 0;
const isTransient = status === 0 || status >= 500;
if (!isTransient) {
console.warn('[SessionGuard] Session fetch returned', status, '— redirecting to login');
return <Navigate to="/login" replace />;
}
// Transient error with a cached session: keep rendering — better-auth will
// retry in the background. Children see the last-known session via useSession().
if (session) {
console.warn('[SessionGuard] Transient session fetch error, keeping user in place:', error);
return <>{children}</>;
}
// Transient error with no cached session: showing children would render the
// app with a null user (broken inbox, failing tRPC calls). Show a retry
// state with an escape hatch — manual link, plus a 30s timeout that falls
// back to /login so a persistent backend outage doesn't strand the user.
console.warn('[SessionGuard] Transient session fetch error and no cached session:', error);
return <TransientSessionErrorFallback />;
}
if (!session) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}
function TransientSessionErrorFallback() {
const [shouldRedirect, setShouldRedirect] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setShouldRedirect(true), 30_000);
return () => clearTimeout(timer);
}, []);
if (shouldRedirect) {
return <Navigate to="/login" replace />;
}
return (
<div className="flex h-screen w-screen items-center justify-center">
<div className="text-center">
<div className="border-accent mx-auto mb-2 h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
<div className="text-muted-foreground text-sm">Connection problem, retrying…</div>
<Link to="/login" replace className="text-muted-foreground mt-3 inline-block text-sm underline">
Go to login
</Link>
</div>
</div>
);
}
/**
* Hook to manually invalidate session cache (e.g., on logout)
* Components can use useSession() directly for session data
*/
export function useInvalidateSession() {
const { refetch } = useSession();
return () => {
// Trigger a refetch of the session data
refetch();
};
}
// Legacy export for backward compatibility - prefer using useSession() directly
export const SessionProvider = SessionGuard;