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:
Zimeng Xiong
2026-01-14 11:25:27 -08:00
committed by GitHub
parent e75b727a5a
commit 0476315322
37 changed files with 2074 additions and 685 deletions
+85
View File
@@ -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();