Files
emaui/libs/auth/src/lib/components/AuthBootstrap.tsx

77 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState, type ReactNode } from 'react';
import { useDispatch } from 'react-redux';
import { authStorage } from '../utils/auth-storage';
import { hydrateAuth, logout, setUser } from '../store/auth.slice';
import type { AuthUser } from '../types/auth.types';
const BASE_API_URL =
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
'http://localhost:3000/api';
/**
* Restores the signed-in session before the router renders.
*
* Redux starts empty on every page load while the token lives in
* localStorage, so without this a refresh leaves the app halfsigned-in: the
* route guards see a token and let you through, but pages that read
* `auth.user` think you are a stranger.
*
* A token the server no longer accepts is cleared here rather than left to
* strand the user on a page they cannot act on or sign out of.
*/
export function AuthBootstrap({ children }: { children: ReactNode }) {
const dispatch = useDispatch();
const [ready, setReady] = useState(false);
useEffect(() => {
let cancelled = false;
async function restore() {
dispatch(hydrateAuth());
const token = authStorage.getToken();
const cachedUser = authStorage.getUser<AuthUser>();
if (!token) {
if (!cancelled) setReady(true);
return;
}
try {
const response = await fetch(`${BASE_API_URL}/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const user = (await response.json()) as AuthUser;
// Keep the persisted session as the source of truth when it is
// available. A successful profile update writes it immediately,
// while `/auth/me` can briefly return a stale read and otherwise
// undo that update on every page refresh. We still make this call
// to validate the token and clear invalid sessions below.
if (!cancelled && !cachedUser) dispatch(setUser(user));
} else if (response.status === 401 || response.status === 403) {
// Expired or revoked — drop it so the user gets a login screen
// instead of a dead end.
if (!cancelled) dispatch(logout());
}
} catch {
// Offline or the API is down: keep the stored session and let the
// individual screens surface their own errors.
} finally {
if (!cancelled) setReady(true);
}
}
restore();
return () => {
cancelled = true;
};
}, [dispatch]);
// Rendering the router before the session resolves would let the guards
// redirect based on a state that is about to change.
if (!ready) return null;
return <>{children}</>;
}