barsa c7230f391a Refactor global exception handling and support case management
- Replaced multiple global exception filters with a unified exception filter to streamline error handling across the application.
- Removed deprecated AuthErrorFilter and GlobalExceptionFilter to reduce redundancy.
- Enhanced SupportController to include new endpoints for listing, retrieving, and creating support cases, improving the support case management functionality.
- Integrated SalesforceCaseService for better interaction with Salesforce data in support case operations.
- Updated support case schemas to align with new requirements and ensure data consistency.
2025-11-26 16:36:06 +09:00

166 lines
5.1 KiB
TypeScript

export { createClient, resolveBaseUrl } from "./runtime/client";
export type {
ApiClient,
AuthHeaderResolver,
CreateClientOptions,
QueryParams,
PathParams,
} from "./runtime/client";
export { ApiError, isApiError } from "./runtime/client";
// Re-export API helpers
export * from "./response-helpers";
// Import createClient for internal use
import { createClient, ApiError } from "./runtime/client";
import { logger } from "@/lib/logger";
/**
* Auth endpoints that should NOT trigger automatic logout on 401
* These are endpoints where 401 means "invalid credentials", not "session expired"
*/
const AUTH_ENDPOINTS = [
"/api/auth/login",
"/api/auth/signup",
"/api/auth/link-whmcs",
"/api/auth/set-password",
"/api/auth/reset-password",
"/api/auth/check-password-needed",
];
/**
* Check if a URL path is an auth endpoint
*/
function isAuthEndpoint(url: string): boolean {
try {
const urlPath = new URL(url).pathname;
return AUTH_ENDPOINTS.some(endpoint => urlPath.endsWith(endpoint));
} catch {
return AUTH_ENDPOINTS.some(endpoint => url.includes(endpoint));
}
}
/**
* Extract error message from API error body
* Handles both `{ message }` and `{ error: { message } }` formats
*/
function extractErrorMessage(body: unknown): string | null {
if (!body || typeof body !== "object") {
return null;
}
// Check for nested error.message format (standard API error response)
const bodyWithError = body as { error?: { message?: unknown } };
if (bodyWithError.error && typeof bodyWithError.error === "object") {
const errorMessage = bodyWithError.error.message;
if (typeof errorMessage === "string") {
return errorMessage;
}
}
// Check for top-level message
const bodyWithMessage = body as { message?: unknown };
if (typeof bodyWithMessage.message === "string") {
return bodyWithMessage.message;
}
return null;
}
/**
* Global error handler for API client
* Handles authentication errors and triggers logout when needed
*/
async function handleApiError(response: Response): Promise<void> {
// Don't import useAuthStore at module level to avoid circular dependencies
// We'll handle auth errors by dispatching a custom event that the auth system can listen to
// Only dispatch logout event for 401s on non-auth endpoints
// Auth endpoints (login, signup, etc.) return 401 for invalid credentials,
// which should NOT trigger logout - just show the error message
if (response.status === 401 && !isAuthEndpoint(response.url)) {
logger.warn("Received 401 Unauthorized response - triggering logout");
// Dispatch a custom event that the auth system will listen to
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent("auth:unauthorized", {
detail: { url: response.url, status: response.status },
})
);
}
}
// Still throw the error so the calling code can handle it
let body: unknown;
let message = response.statusText || `Request failed with status ${response.status}`;
try {
const cloned = response.clone();
const contentType = cloned.headers.get("content-type");
if (contentType?.includes("application/json")) {
body = await cloned.json();
const extractedMessage = extractErrorMessage(body);
if (extractedMessage) {
message = extractedMessage;
}
}
} catch {
// Ignore body parse errors
}
throw new ApiError(message, response, body);
}
export const apiClient = createClient({
handleError: handleApiError,
});
// Query keys for React Query - matching the expected structure
export const queryKeys = {
auth: {
me: () => ["auth", "me"] as const,
session: () => ["auth", "session"] as const,
},
billing: {
invoices: (params?: Record<string, unknown>) => ["billing", "invoices", params] as const,
invoice: (id: string) => ["billing", "invoice", id] as const,
paymentMethods: () => ["billing", "payment-methods"] as const,
},
subscriptions: {
all: () => ["subscriptions"] as const,
list: (params?: Record<string, unknown>) => ["subscriptions", "list", params] as const,
active: () => ["subscriptions", "active"] as const,
stats: () => ["subscriptions", "stats"] as const,
detail: (id: string) => ["subscriptions", "detail", id] as const,
invoices: (id: number, params?: Record<string, unknown>) =>
["subscriptions", "invoices", id, params] as const,
},
dashboard: {
summary: () => ["dashboard", "summary"] as const,
},
catalog: {
products: () => ["catalog", "products"] as const,
internet: {
combined: () => ["catalog", "internet", "combined"] as const,
},
sim: {
combined: () => ["catalog", "sim", "combined"] as const,
},
vpn: {
combined: () => ["catalog", "vpn", "combined"] as const,
},
},
orders: {
list: () => ["orders", "list"] as const,
detail: (id: string | number) => ["orders", "detail", String(id)] as const,
},
support: {
cases: (params?: Record<string, unknown>) => ["support", "cases", params] as const,
case: (id: string) => ["support", "case", id] as const,
},
currency: {
default: () => ["currency", "default"] as const,
},
} as const;