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
+53 -41
View File
@@ -1,7 +1,13 @@
import { test, expect } from "@playwright/test";
import * as fs from "fs";
import * as path from "path";
import { API_URL, createDrawing, deleteDrawing, getDrawing } from "./helpers/api";
import {
API_URL,
createDrawing,
deleteDrawing,
getCsrfHeaders,
getDrawing,
} from "./helpers/api";
/**
* E2E Browser Tests for Image Persistence - Issue #17 Regression
@@ -28,13 +34,13 @@ function generateLargeImageDataUrl(sizeInBytes: number = 50000): string {
test.describe("Image Persistence - Browser E2E Tests", () => {
let testDrawingIds: string[] = [];
test.afterEach(async ({ request }) => {
// Clean up any drawings created during tests
for (const id of testDrawingIds) {
try {
await deleteDrawing(request, id);
} catch (e) {
} catch {
// Ignore cleanup errors
}
}
@@ -43,23 +49,23 @@ test.describe("Image Persistence - Browser E2E Tests", () => {
test("should navigate to dashboard and see drawing list", async ({ page }) => {
await page.goto("/");
// Wait for the page to load
await expect(page).toHaveTitle(/ExcaliDash/i);
// The dashboard should show some UI elements
await expect(page.locator("body")).toBeVisible();
});
test("should create a new drawing via UI", async ({ page }) => {
await page.goto("/");
// Look for a "New Drawing" or similar button
const newDrawingBtn = page.getByRole("button", { name: /new|create/i }).first();
if (await newDrawingBtn.isVisible()) {
await newDrawingBtn.click();
// Should navigate to editor or show a modal
await page.waitForURL(/\/(editor|drawing)/i, { timeout: 5000 }).catch(() => {
// May stay on same page with modal
@@ -71,7 +77,7 @@ test.describe("Image Persistence - Browser E2E Tests", () => {
// This is the core regression test for issue #17
const largeDataUrl = generateLargeImageDataUrl(50000);
expect(largeDataUrl.length).toBeGreaterThan(10000);
const files = {
"test-image-1": {
id: "test-image-1",
@@ -80,23 +86,23 @@ test.describe("Image Persistence - Browser E2E Tests", () => {
created: Date.now(),
},
};
// Create drawing with large image
const createdDrawing = await createDrawing(request, {
name: "E2E Test - Large Image",
files,
});
testDrawingIds.push(createdDrawing.id);
// Retrieve the drawing
const drawing = await getDrawing(request, createdDrawing.id);
const savedFiles = drawing.files || {}; // Already parsed by API
// Verify the image data was preserved
expect(savedFiles["test-image-1"]).toBeDefined();
expect(savedFiles["test-image-1"].dataURL).toBe(largeDataUrl);
expect(savedFiles["test-image-1"].dataURL.length).toBe(largeDataUrl.length);
console.log("✓ Large image data preserved correctly through save/reload cycle");
});
@@ -106,36 +112,36 @@ test.describe("Image Persistence - Browser E2E Tests", () => {
name: "E2E Test - Editor View",
});
testDrawingIds.push(createdDrawing.id);
// Navigate to the editor
await page.goto(`/editor/${createdDrawing.id}`);
// Wait for the page to load
await page.waitForLoadState("networkidle");
// The editor should be visible (Excalidraw canvas)
// Look for the Excalidraw container or canvas
const editorContainer = page.locator("[class*='excalidraw'], canvas").first();
await expect(editorContainer).toBeVisible({ timeout: 10000 });
});
test("should import .excalidraw file with embedded image", async ({ page, request }) => {
test("should import .excalidraw file with embedded image", async ({ request }) => {
// Load the test fixture
const fixturePath = path.join(__dirname, "..", "fixtures", "small-image.excalidraw");
const fixtureContent = fs.readFileSync(fixturePath, "utf-8");
const fixtureData = JSON.parse(fixtureContent);
// Create drawing via API with fixture data
const createdDrawing = await createDrawing(request, {
name: "E2E Test - Imported Image",
files: fixtureData.files,
});
testDrawingIds.push(createdDrawing.id);
// Create drawing via API with fixture data
const createdDrawing = await createDrawing(request, {
name: "E2E Test - Imported Image",
files: fixtureData.files,
});
testDrawingIds.push(createdDrawing.id);
// Verify via API that image data was preserved
const drawing = await getDrawing(request, createdDrawing.id);
const drawing = await getDrawing(request, createdDrawing.id);
const savedFiles = drawing.files || {}; // Already parsed by API
expect(savedFiles["embedded-test-image"]).toBeDefined();
expect(savedFiles["embedded-test-image"].dataURL).toBe(fixtureData.files["embedded-test-image"].dataURL);
});
@@ -161,23 +167,23 @@ test.describe("Image Persistence - Browser E2E Tests", () => {
created: Date.now(),
},
};
const createdDrawing = await createDrawing(request, {
name: "E2E Test - Multiple Images",
files,
});
testDrawingIds.push(createdDrawing.id);
const drawing = await getDrawing(request, createdDrawing.id);
const savedFiles = drawing.files || {}; // Already parsed by API
// Verify all images preserved correctly
for (const [id, originalFile] of Object.entries(files)) {
expect(savedFiles[id]).toBeDefined();
expect(savedFiles[id].dataURL).toBe((originalFile as any).dataURL);
expect(savedFiles[id].dataURL.length).toBe((originalFile as any).dataURL.length);
}
console.log("✓ Multiple images of varying sizes preserved correctly");
});
});
@@ -192,10 +198,11 @@ test.describe("Security - Malicious Content Blocking", () => {
created: Date.now(),
},
};
const response = await request.post(`${API_URL}/drawings`, {
headers: {
"Content-Type": "application/json",
...(await getCsrfHeaders(request)),
},
data: {
name: "Security Test - JS URL",
@@ -205,7 +212,7 @@ test.describe("Security - Malicious Content Blocking", () => {
preview: null,
},
});
if (!response.ok()) {
const text = await response.text();
console.error(`API Error: ${response.status()} - ${text}`);
@@ -213,12 +220,14 @@ test.describe("Security - Malicious Content Blocking", () => {
expect(response.ok()).toBe(true);
const drawing = await response.json();
const savedFiles = drawing.files; // Already parsed by API
// The malicious URL should be blocked/cleared
expect(savedFiles["malicious-image"].dataURL).not.toContain("javascript:");
// Cleanup
await request.delete(`${API_URL}/drawings/${drawing.id}`);
await request.delete(`${API_URL}/drawings/${drawing.id}`, {
headers: await getCsrfHeaders(request),
});
});
test("should block script tags in image data", async ({ request }) => {
@@ -230,10 +239,11 @@ test.describe("Security - Malicious Content Blocking", () => {
created: Date.now(),
},
};
const response = await request.post(`${API_URL}/drawings`, {
headers: {
"Content-Type": "application/json",
...(await getCsrfHeaders(request)),
},
data: {
name: "Security Test - Script Tag",
@@ -243,7 +253,7 @@ test.describe("Security - Malicious Content Blocking", () => {
preview: null,
},
});
if (!response.ok()) {
const text = await response.text();
console.error(`API Error: ${response.status()} - ${text}`);
@@ -251,11 +261,13 @@ test.describe("Security - Malicious Content Blocking", () => {
expect(response.ok()).toBe(true);
const drawing = await response.json();
const savedFiles = drawing.files; // Already parsed by API
// The script tag should be blocked
expect(savedFiles["malicious-image"].dataURL).not.toContain("<script>");
// Cleanup
await request.delete(`${API_URL}/drawings/${drawing.id}`);
await request.delete(`${API_URL}/drawings/${drawing.id}`, {
headers: await getCsrfHeaders(request),
});
});
});