Files
emaui/libs/auth/src/lib/components/AuthBootstrap.tsx
mihretue c753009ed9 refactor(api): resolve the backend URL in one place
VITE_BASE_API_URL with a localhost:3001/api fallback was re-derived in six
files across libs/api, libs/auth, the portal and the backoffice — and a seventh
site (UserManagementPage) read the env raw with no fallback at all, handing the
IAM user-management app an undefined apiUrl whenever no .env was present.

BASE_API_URL in base-query-with-reauth.ts is now the single definition,
exported from @ema-platform/api; every other site imports it. Trims the env
value and treats blank as unset, matching how the backend reads its own keys.
2026-08-26 09:09:25 +00:00

98 lines
3.8 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 { PageLoader } from '@ema-platform/ui';
import { authStorage } from '../utils/auth-storage';
import { hydrateAuth, logout, setToken, setUser } from '../store/auth.slice';
import { refreshAccessToken } from '../utils/refresh-token';
import type { AuthUser } from '../types/auth.types';
import { BASE_API_URL } from '@ema-platform/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 {
let response = await fetch(`${BASE_API_URL}/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
// An expired access token is the normal state after a day away — spend
// the refresh token before deciding the session is over. Without this
// a lapsed token logs the user out on load even though the credential
// to renew it is sitting right next to it in storage.
if (response.status === 401 || response.status === 403) {
try {
const fresh = await refreshAccessToken();
dispatch(setToken(fresh));
response = await fetch(`${BASE_API_URL}/auth/me`, {
headers: { Authorization: `Bearer ${fresh}` },
});
} catch (err) {
// Only the server rejecting the refresh token ends the session —
// same rule as the API layer's 401 handler. A 502 or a network
// blip during refresh keeps the stored session; the screens
// surface their own errors.
if (!(err as { sessionExpired?: boolean })?.sessionExpired) return;
// Rejected — fall through to the logout below.
}
}
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 <PageLoader label="Authenticating Maritime Session…" height="100vh" />;
return <>{children}</>;
}