mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
fix issue
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { API_BASE_URL } from "@/constants/apiConfig";
|
||||
import {
|
||||
emitApiError,
|
||||
extractApiErrorPayload,
|
||||
} from "@/components/errors/ApiErrorModal";
|
||||
import {
|
||||
AUTH_TOKEN_COOKIE,
|
||||
REFRESH_TOKEN_COOKIE,
|
||||
@@ -86,6 +90,12 @@ api.interceptors.response.use(
|
||||
originalRequest.url?.includes("/auth/mfa-verify") ||
|
||||
originalRequest.url?.includes("/auth/refresh-token")
|
||||
) {
|
||||
// Surface the server's actual error message in the global error modal
|
||||
// (401s are handled by the session-refresh flow, so skip them).
|
||||
if (error.response && error.response.status !== 401) {
|
||||
const payload = extractApiErrorPayload(error);
|
||||
if (payload) emitApiError(payload);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, Button, Group, List, Modal, Stack, Text } from "@mantine/core";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Global API error modal.
|
||||
*
|
||||
* The axios client emits every failed request (with a server response) through
|
||||
* `emitApiError`; this modal — mounted once at the app root — shows the
|
||||
* SERVER'S actual `message` instead of a generic "Request failed with status
|
||||
* code NNN". Pages listed in EXCLUDED_PATH_PATTERNS (warehouse, first/last
|
||||
* mile, onboarding/auth screens) keep their own inline error handling and
|
||||
* never trigger it.
|
||||
*/
|
||||
|
||||
export interface ApiErrorPayload {
|
||||
/** Server messages — the `message` field (string or class-validator array). */
|
||||
messages: string[];
|
||||
statusCode?: number;
|
||||
/** API path that failed, shown as small helper text. */
|
||||
path?: string;
|
||||
}
|
||||
|
||||
type Listener = (payload: ApiErrorPayload) => void;
|
||||
|
||||
let listener: Listener | null = null;
|
||||
|
||||
/** Current-page path patterns where the global modal must stay silent. */
|
||||
const EXCLUDED_PATH_PATTERNS = [
|
||||
/^\/auth/,
|
||||
/^\/callback/,
|
||||
/warehouse/i,
|
||||
/first-mile/i,
|
||||
/last-mile/i,
|
||||
/onboard/i,
|
||||
/register/i,
|
||||
];
|
||||
|
||||
export function isGlobalErrorModalSuppressed(pathname: string): boolean {
|
||||
return EXCLUDED_PATH_PATTERNS.some((re) => re.test(pathname));
|
||||
}
|
||||
|
||||
export function emitApiError(payload: ApiErrorPayload): void {
|
||||
if (isGlobalErrorModalSuppressed(window.location.pathname)) return;
|
||||
listener?.(payload);
|
||||
}
|
||||
|
||||
/** Pull the server `message` out of an axios-style error. */
|
||||
export function extractApiErrorPayload(error: unknown): ApiErrorPayload | null {
|
||||
const err = error as {
|
||||
response?: {
|
||||
status?: number;
|
||||
data?: {
|
||||
message?: string | string[];
|
||||
error?: string;
|
||||
statusCode?: number;
|
||||
path?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
const response = err?.response;
|
||||
if (!response) return null; // network error / cancellation — not ours
|
||||
const data = response.data;
|
||||
const raw = data?.message;
|
||||
const messages = Array.isArray(raw)
|
||||
? raw.filter((m): m is string => typeof m === "string" && m.length > 0)
|
||||
: typeof raw === "string" && raw.length > 0
|
||||
? [raw]
|
||||
: [];
|
||||
if (messages.length === 0) {
|
||||
messages.push(
|
||||
data?.error ?? `Request failed with status code ${response.status}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
messages,
|
||||
statusCode: data?.statusCode ?? response.status,
|
||||
path: data?.path,
|
||||
};
|
||||
}
|
||||
|
||||
export function ApiErrorModal() {
|
||||
const [payload, setPayload] = useState<ApiErrorPayload | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
listener = (next) => {
|
||||
// Don't stack identical messages while the modal is already showing them.
|
||||
setPayload((current) =>
|
||||
current && current.messages.join("\n") === next.messages.join("\n")
|
||||
? current
|
||||
: next,
|
||||
);
|
||||
};
|
||||
return () => {
|
||||
listener = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const close = () => setPayload(null);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={payload !== null}
|
||||
onClose={close}
|
||||
centered
|
||||
radius="md"
|
||||
title={
|
||||
<Group gap="xs">
|
||||
<AlertTriangle size={18} color="var(--mantine-color-red-6)" />
|
||||
<Text fw={700}>Request failed</Text>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
{payload && (
|
||||
<Stack gap="md">
|
||||
<Alert color="red" variant="light">
|
||||
{payload.messages.length === 1 ? (
|
||||
<Text size="sm">{payload.messages[0]}</Text>
|
||||
) : (
|
||||
<List size="sm" spacing={4}>
|
||||
{payload.messages.map((m, i) => (
|
||||
<List.Item key={i}>{m}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Alert>
|
||||
{(payload.statusCode || payload.path) && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{payload.statusCode ? `Status ${payload.statusCode}` : null}
|
||||
{payload.statusCode && payload.path ? " · " : null}
|
||||
{payload.path}
|
||||
</Text>
|
||||
)}
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={close}>
|
||||
Close
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { Toaster } from "react-hot-toast";
|
||||
import "./i18n";
|
||||
|
||||
import App from "./App";
|
||||
import { ApiErrorModal } from "./components/errors/ApiErrorModal";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
import { AuthProvider } from "./auth/AuthProvider";
|
||||
import { queryClient } from "./lib/queryClient";
|
||||
@@ -57,6 +58,9 @@ createRoot(rootElement).render(
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
{/* Global API error modal — shows the server's actual error
|
||||
message (suppressed on warehouse / mile / onboarding pages). */}
|
||||
<ApiErrorModal />
|
||||
<Toaster position="top-right" />
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
|
||||
Reference in New Issue
Block a user