Compare commits

..

6 Commits

Author SHA1 Message Date
Zimeng Xiong 0ba96c47a8 "Claude Code Review workflow" 2026-01-20 10:55:24 -08:00
Zimeng Xiong 0f93f1ab76 "Claude PR Assistant workflow" 2026-01-20 10:55:22 -08:00
Zimeng Xiong 7c238701b7 Update RELEASE.md with CSRF_SECRET instructions (#33)
Added instructions for the required CSRF_SECRET environment variable for CSRF protection in Kubernetes deployments.
2026-01-14 13:11:25 -08:00
Zimeng Xiong c5c8b15e75 Update README header to remove version number
Removed version number from README header.
2026-01-14 13:10:43 -08:00
Zimeng Xiong 9bc3c7c8fc chore: release v0.3.0 2026-01-14 11:26:20 -08:00
Zimeng Xiong 0476315322 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>
2026-01-14 11:25:27 -08:00
11 changed files with 139 additions and 27 deletions
+44
View File
@@ -0,0 +1,44 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
+50
View File
@@ -0,0 +1,50 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'
+1 -1
View File
@@ -1,6 +1,6 @@
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
# ExcaliDash v0.1.8
# ExcaliDash
![License](https://img.shields.io/github/license/zimengxiong/ExcaliDash)
![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)
+14
View File
@@ -27,3 +27,17 @@ CSRF Protection (8a78b2b)
- Updated docker-compose configurations with new environment variables
- E2E test suite improvements and reliability fixes
- Added Kubernetes deployment note in README
### Kubernetes
A `CSRF_SECRET` environment variable is now required for CSRF protection. Generate a secure 32+ character random string:
```bash
openssl rand -base64 32
Add it to your deployment:
- Docker Compose: Add CSRF_SECRET=<your-secret> to the backend service environment
- Kubernetes: Add to your ConfigMap/Secret and reference in the backend deployment
If not set, the backend will refuse to start.
```
+1 -1
View File
@@ -1 +1 @@
0.2.1
0.3.0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "backend",
"version": "0.2.1",
"version": "0.3.0",
"description": "",
"main": "index.js",
"scripts": {
-1
View File
@@ -532,7 +532,6 @@ export const validateImportedDrawing = (data: any): boolean => {
// CSRF Protection
// ============================================================================
const CSRF_TOKEN_LENGTH = 32;
const CSRF_TOKEN_HEADER = "x-csrf-token";
const CSRF_TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
const CSRF_TOKEN_FUTURE_SKEW_MS = 5 * 60 * 1000; // 5 minutes clock skew tolerance
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.2.1",
"version": "0.3.0",
"type": "module",
"scripts": {
"dev": "vite",
+6 -6
View File
@@ -48,7 +48,7 @@ export const UploadProvider: React.FC<{ children: ReactNode }> = ({ children })
const uploadFiles = useCallback(async (files: File[], targetCollectionId: string | null) => {
const newTasks: UploadTask[] = files.map(f => ({
id: Math.random().toString(36).substring(2, 11),
id: crypto.randomUUID(),
fileName: f.name,
status: 'pending',
progress: 0
@@ -56,12 +56,12 @@ export const UploadProvider: React.FC<{ children: ReactNode }> = ({ children })
setTasks(prev => [...newTasks, ...prev]);
// Map file names to task IDs for progress callbacks
const fileTaskMap = new Map<string, string>();
newTasks.forEach(t => fileTaskMap.set(t.fileName, t.id));
// Map file index to task ID for progress callbacks (handles duplicate filenames)
const indexToTaskId = new Map<number, string>();
newTasks.forEach((t, index) => indexToTaskId.set(index, t.id));
const handleProgress = (fileName: string, status: UploadStatus, progress: number, error?: string) => {
const taskId = fileTaskMap.get(fileName);
const handleProgress = (fileIndex: number, status: UploadStatus, progress: number, error?: string) => {
const taskId = indexToTaskId.get(fileIndex);
if (taskId) {
updateTask(taskId, { status, progress, error });
}
+15 -7
View File
@@ -7,7 +7,7 @@ export const importDrawings = async (
targetCollectionId: string | null,
onSuccess?: () => void | Promise<void>,
onProgress?: (
fileName: string,
fileIndex: number,
status: UploadStatus,
progress: number,
error?: string
@@ -25,12 +25,20 @@ export const importDrawings = async (
let failCount = 0;
const errors: string[] = [];
// Build a map from drawingFile index to original file index for progress reporting
const originalIndexMap = new Map<number, number>();
drawingFiles.forEach((df, i) => {
const originalIndex = files.indexOf(df);
originalIndexMap.set(i, originalIndex);
});
// We process files in parallel (Promise.all) but we could limit concurrency if needed.
// For now, full parallel is fine as browser limits connection count anyway.
await Promise.all(
drawingFiles.map(async (file) => {
drawingFiles.map(async (file, drawingIndex) => {
const fileIndex = originalIndexMap.get(drawingIndex) ?? drawingIndex;
try {
if (onProgress) onProgress(file.name, 'processing', 0); // Parsing phase
if (onProgress) onProgress(fileIndex, 'processing', 0); // Parsing phase
const text = await file.text();
const data = JSON.parse(text);
@@ -61,7 +69,7 @@ export const importDrawings = async (
preview: svg.outerHTML,
};
if (onProgress) onProgress(file.name, 'uploading', 0);
if (onProgress) onProgress(fileIndex, 'uploading', 0);
await api.post("/drawings", payload, {
headers: {
@@ -73,12 +81,12 @@ export const importDrawings = async (
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(file.name, 'uploading', percentCompleted);
onProgress(fileIndex, 'uploading', percentCompleted);
}
},
});
if (onProgress) onProgress(file.name, 'success', 100);
if (onProgress) onProgress(fileIndex, 'success', 100);
successCount++;
} catch (err: any) {
@@ -90,7 +98,7 @@ export const importDrawings = async (
err?.message ||
"Upload failed";
errors.push(`${file.name}: ${errorMessage}`);
if (onProgress) onProgress(file.name, 'error', 0, errorMessage);
if (onProgress) onProgress(fileIndex, 'error', 0, errorMessage);
}
})
);
+6 -9
View File
@@ -15,19 +15,16 @@ try {
console.warn("Unable to read VERSION file:", error);
}
if (
!process.env.VITE_APP_VERSION ||
process.env.VITE_APP_VERSION.trim().length === 0
) {
process.env.VITE_APP_VERSION = versionFromFile;
if (!process.env.VITE_APP_BUILD_LABEL) {
process.env.VITE_APP_BUILD_LABEL = "local development build";
}
}
const appVersion = process.env.VITE_APP_VERSION?.trim() || versionFromFile;
const buildLabel = process.env.VITE_APP_BUILD_LABEL?.trim() || "local development build";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
define: {
'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion),
'import.meta.env.VITE_APP_BUILD_LABEL': JSON.stringify(buildLabel),
},
server: {
proxy: {
"/api": {