0.2.1 Release (#32)
* feat(security): implement CSRF protection * chore: clean up CSRF implementation - Remove unused generateCsrfToken export from security.ts - Remove redundant /csrf-token path check (GET already exempt) - Restore defineConfig wrapper in vitest.config.ts for type safety * add K8S note in README, fix broken e2e * feat/upload-bar (#30) * feat/upload-bar: add a upload bar when user upload file, indicate the upload process * feat/save-loading-status: add save status when click back button from editor * fix: address PR review issues in upload and save features - Replace deprecated substr() with substring() in UploadContext - Fix broken error handling that checked stale task status - Fix missing useEffect dependency in UploadStatus - Fix CSS class conflict in progress bar styling - Add error recovery for save state in Editor (reset on failure) - Use .finally() instead of .then() to ensure refresh on upload failure - Fix inconsistent indentation in UploadContext * fix e2e tests --------- Co-authored-by: Zimeng Xiong <zxzimeng@gmail.com> * chore: pre-release v0.2.1-dev * Update backend/src/security.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix filename/math random UUID generation --------- Co-authored-by: AdrianAcala <adrianacala017@gmail.com> Co-authored-by: adamant368 <60790941+Yiheng-Liu@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,91 @@ export const api = axios.create({
|
||||
baseURL: API_URL,
|
||||
});
|
||||
|
||||
// CSRF Token Management
|
||||
let csrfToken: string | null = null;
|
||||
let csrfHeaderName: string = "x-csrf-token";
|
||||
let csrfTokenPromise: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Fetch a fresh CSRF token from the server
|
||||
*/
|
||||
export const fetchCsrfToken = async (): Promise<void> => {
|
||||
try {
|
||||
const response = await axios.get<{ token: string; header: string }>(
|
||||
`${API_URL}/csrf-token`
|
||||
);
|
||||
csrfToken = response.data.token;
|
||||
csrfHeaderName = response.data.header || "x-csrf-token";
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch CSRF token:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure we have a valid CSRF token, fetching one if needed
|
||||
*/
|
||||
const ensureCsrfToken = async (): Promise<void> => {
|
||||
if (csrfToken) return;
|
||||
|
||||
// Prevent multiple simultaneous token fetches
|
||||
if (!csrfTokenPromise) {
|
||||
csrfTokenPromise = fetchCsrfToken().finally(() => {
|
||||
csrfTokenPromise = null;
|
||||
});
|
||||
}
|
||||
await csrfTokenPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the cached CSRF token (useful for handling 403 errors)
|
||||
*/
|
||||
export const clearCsrfToken = (): void => {
|
||||
csrfToken = null;
|
||||
};
|
||||
|
||||
// Add request interceptor to include CSRF token
|
||||
api.interceptors.request.use(
|
||||
async (config) => {
|
||||
// Only add CSRF token for state-changing methods
|
||||
const method = config.method?.toUpperCase();
|
||||
if (method && ["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
|
||||
await ensureCsrfToken();
|
||||
if (csrfToken) {
|
||||
config.headers[csrfHeaderName] = csrfToken;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Add response interceptor to handle CSRF token errors
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
// If we get a 403 with CSRF error, clear token and retry once
|
||||
if (
|
||||
error.response?.status === 403 &&
|
||||
error.response?.data?.error?.includes("CSRF")
|
||||
) {
|
||||
clearCsrfToken();
|
||||
|
||||
// Retry the request once with a fresh token
|
||||
const originalRequest = error.config;
|
||||
if (!originalRequest._csrfRetry) {
|
||||
originalRequest._csrfRetry = true;
|
||||
await fetchCsrfToken();
|
||||
if (csrfToken) {
|
||||
originalRequest.headers[csrfHeaderName] = csrfToken;
|
||||
}
|
||||
return api(originalRequest);
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
const coerceTimestamp = (value: string | number | Date): number => {
|
||||
if (typeof value === "number") return value;
|
||||
if (value instanceof Date) return value.getTime();
|
||||
|
||||
Reference in New Issue
Block a user