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
+13 -31
View File
@@ -9,7 +9,7 @@ import type { DrawingSummary, Collection } from '../types';
import { useDebounce } from '../hooks/useDebounce';
import clsx from 'clsx';
import { ConfirmModal } from '../components/ConfirmModal';
import { importDrawings } from '../utils/importUtils';
import { useUpload } from '../context/UploadContext';
type Point = { x: number; y: number };
@@ -78,7 +78,6 @@ export const Dashboard: React.FC = () => {
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
const [showImportError, setShowImportError] = useState<{ isOpen: boolean; message: string }>({ isOpen: false, message: '' });
const [showImportSuccess, setShowImportSuccess] = useState(false);
const [isDragSelecting, setIsDragSelecting] = useState(false);
const [dragStart, setDragStart] = useState<Point | null>(null);
@@ -99,6 +98,8 @@ export const Dashboard: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const { uploadFiles } = useUpload();
const refreshData = useCallback(async () => {
setIsLoading(true);
try {
@@ -303,17 +304,12 @@ export const Dashboard: React.FC = () => {
const fileArray = Array.from(files);
const targetCollectionId = selectedCollectionId === undefined ? null : selectedCollectionId;
const result = await importDrawings(fileArray, targetCollectionId, refreshData);
if (result.failed > 0) {
setShowImportError({
isOpen: true,
message: `Import complete with errors.\nSuccess: ${result.success}\nFailed: ${result.failed}\nErrors:\n${result.errors.join('\n')}`
});
} else {
setShowImportSuccess(true);
}
// Use the global upload context
uploadFiles(fileArray, targetCollectionId).finally(() => {
// Refresh after all uploads complete (success or failure)
refreshData();
});
};
const handleRenameDrawing = async (id: string, name: string) => {
@@ -525,8 +521,7 @@ export const Dashboard: React.FC = () => {
// Handle Files
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const files = Array.from(e.dataTransfer.files);
setIsLoading(true);
const libFiles = files.filter(f => f.name.endsWith('.excalidrawlib'));
if (libFiles.length > 0) {
setShowImportError({
@@ -537,13 +532,11 @@ export const Dashboard: React.FC = () => {
const drawingFiles = files.filter(f => !f.name.endsWith('.excalidrawlib'));
if (drawingFiles.length > 0) {
const result = await importDrawings(drawingFiles, targetCollectionId, refreshData);
if (result.failed > 0) {
alert(`Import complete with errors.\nSuccess: ${result.success}\nFailed: ${result.failed}\nErrors:\n${result.errors.join('\n')}`);
}
uploadFiles(drawingFiles, targetCollectionId).finally(() => {
refreshData();
});
}
setIsLoading(false);
return;
}
@@ -938,17 +931,6 @@ export const Dashboard: React.FC = () => {
onCancel={() => setShowImportError({ isOpen: false, message: '' })}
/>
<ConfirmModal
isOpen={showImportSuccess}
title="Import Successful"
message="Drawings imported successfully."
confirmText="OK"
showCancel={false}
isDangerous={false}
variant="success"
onConfirm={() => setShowImportSuccess(false)}
onCancel={() => setShowImportSuccess(false)}
/>
</Layout>
);
};
+31 -13
View File
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Download } from 'lucide-react';
import { ArrowLeft, Download, Loader2 } from 'lucide-react';
import { Excalidraw, exportToSvg } from '@excalidraw/excalidraw';
import '@excalidraw/excalidraw/index.css';
import debounce from 'lodash/debounce';
@@ -56,6 +56,7 @@ export const Editor: React.FC = () => {
const [newName, setNewName] = useState('');
const [initialData, setInitialData] = useState<any>(null);
const [isSceneLoading, setIsSceneLoading] = useState(true);
const [isSavingOnLeave, setIsSavingOnLeave] = useState(false);
useEffect(() => {
document.title = `${drawingName} - ExcaliDash`;
@@ -591,23 +592,29 @@ export const Editor: React.FC = () => {
// Disable native Excalidraw save dialogs
const handleBackClick = async () => {
if (isSavingOnLeave) return; // Prevent double clicks
setIsSavingOnLeave(true);
// Save drawing and generate preview before navigating
if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) {
const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted();
const appState = excalidrawAPI.current.getAppState();
const files = excalidrawAPI.current.getFiles() || {};
latestElementsRef.current = elements;
latestFilesRef.current = files;
try {
try {
if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) {
const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted();
const appState = excalidrawAPI.current.getAppState();
const files = excalidrawAPI.current.getFiles() || {};
latestElementsRef.current = elements;
latestFilesRef.current = files;
await Promise.all([
saveDataRef.current(elements, appState),
savePreviewRef.current(elements, appState, files)
]);
console.log("[Editor] Saved on back navigation", { drawingId: id });
} catch (err) {
console.error('Failed to save on back navigation', err);
}
} catch (err) {
console.error('Failed to save on back navigation', err);
} finally {
setIsSavingOnLeave(false);
}
navigate('/');
};
@@ -616,8 +623,19 @@ export const Editor: React.FC = () => {
<div className="h-screen flex flex-col bg-white dark:bg-neutral-950 overflow-hidden">
<header className="h-14 bg-white dark:bg-neutral-900 border-b border-gray-200 dark:border-neutral-800 flex items-center px-4 justify-between z-10">
<div className="flex items-center gap-4">
<button onClick={handleBackClick} className="p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 rounded-full text-gray-600 dark:text-gray-300">
<ArrowLeft size={20} />
<button
onClick={handleBackClick}
disabled={isSavingOnLeave}
className={`flex items-center gap-2 p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 rounded-full text-gray-600 dark:text-gray-300 disabled:opacity-50 disabled:cursor-wait transition-all duration-200 ${isSavingOnLeave ? 'pr-4' : ''}`}
>
{isSavingOnLeave ? (
<>
<Loader2 size={20} className="animate-spin" />
<span className="text-sm font-medium">Saving changes...</span>
</>
) : (
<ArrowLeft size={20} />
)}
</button>
{isRenaming ? (