mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
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 half–signed-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}</>;
|
||
}
|