fix(#338): Route all state-changing fetch() calls through API client
- Replace raw fetch() with apiPost/apiPatch/apiDelete in: - ImportExportActions.tsx: POST for file imports - KanbanBoard.tsx: PATCH for task status updates - ActiveProjectsWidget.tsx: POST for widget data fetches - useLayouts.ts: POST/PATCH/DELETE for layout management - Add apiPostFormData() method to API client for FormData uploads - Ensures CSRF token is included in all state-changing requests - Update tests to mock CSRF token fetch for API client usage Refs #338 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -214,3 +214,57 @@ export async function apiDelete<T>(endpoint: string, workspaceId?: string): Prom
|
||||
}
|
||||
return apiRequest<T>(endpoint, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request helper for FormData uploads
|
||||
* Note: This does not set Content-Type header to allow browser to set multipart/form-data boundary
|
||||
*/
|
||||
export async function apiPostFormData<T>(
|
||||
endpoint: string,
|
||||
formData: FormData,
|
||||
workspaceId?: string
|
||||
): Promise<T> {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
// Add workspace ID header if provided
|
||||
if (workspaceId) {
|
||||
headers["X-Workspace-Id"] = workspaceId;
|
||||
}
|
||||
|
||||
// Add CSRF token for state-changing request
|
||||
const token = await ensureCsrfToken();
|
||||
headers["X-CSRF-Token"] = token;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: formData,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error: ApiError = await response.json().catch(
|
||||
(): ApiError => ({
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: response.statusText || "An unknown error occurred",
|
||||
})
|
||||
);
|
||||
|
||||
// Handle CSRF token mismatch - refresh token and retry once
|
||||
if (
|
||||
response.status === 403 &&
|
||||
(error.code === "CSRF_ERROR" || error.message.includes("CSRF"))
|
||||
) {
|
||||
// Refresh CSRF token
|
||||
await fetchCsrfToken();
|
||||
|
||||
// Retry the request with new token (recursive call)
|
||||
return apiPostFormData<T>(endpoint, formData, workspaceId);
|
||||
}
|
||||
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user