Compare commits

..

67 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
dependabot[bot] e75b727a5a Bump body-parser from 2.2.0 to 2.2.1 in /backend (#11)
Bumps [body-parser](https://github.com/expressjs/body-parser) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-version: 2.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 15:22:06 -08:00
dependabot[bot] c2aa742a79 Bump express from 5.1.0 to 5.2.0 in /backend (#16)
Bumps [express](https://github.com/expressjs/express) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/v5.1.0...v5.2.0)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 5.2.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 15:21:55 -08:00
Zimeng Xiong 49b413bf07 Testing infrastructure, fix truncating of dataURLs (#26)
* feat: implement comprehensive testing infrastructure

- Fix image dataURL truncation bug in security.ts with configurable size limits
- Add backend integration tests (22 tests) with Vitest for API validation
- Add frontend unit tests (11 tests) for JSON serialization
- Implement browser-based E2E tests (8 tests) with Playwright
- Create Docker setup for repeatable E2E testing environment
- Add GitHub Actions CI workflow for automated testing
- Update .gitignore for test artifacts and temporary files

Testing Infrastructure:
- Backend: Vitest + Supertest for API integration tests
- Frontend: Vitest + Testing Library for component tests
- E2E: Playwright with Chromium for full browser automation
- CI/CD: GitHub Actions with parallel test execution

Security Improvements:
- Make dataURL size limit configurable (default: 10MB)
- Enhanced validation for image dataURLs
- Block malicious content (javascript:, script tags)

All tests pass: 41 total (22 backend + 11 frontend + 8 E2E)

* feat(tests): add comprehensive E2E tests for dashboard workflows and image persistence
chore(env): update environment variables for consistent API URL usage
fix(api): centralize API request helpers for drawing and collection management
style(DrawingCard): enhance accessibility with ARIA attributes and data-testid for testing

* cleanup/revise documentation

* cleanup/revise documentation

* Add end-to-end tests for drawing CRUD, export/import, search/sort, and theme toggle functionalities

- Implemented E2E tests for drawing creation, editing, and deletion in `drawing-crud.spec.ts`.
- Added tests for export and import features, including JSON and SQLite formats in `export-import.spec.ts`.
- Created tests for searching and sorting drawings by name and date in `search-and-sort.spec.ts`.
- Developed tests for theme toggle functionality to ensure persistence across sessions in `theme-toggle.spec.ts`.

* fix: exclude test files from production build to fix Docker build

* feat: implement comprehensive testing infrastructure (#19)

* bump version 0.1.7

* feat: implement comprehensive testing infrastructure

- Fix image dataURL truncation bug in security.ts with configurable size limits
- Add backend integration tests (22 tests) with Vitest for API validation
- Add frontend unit tests (11 tests) for JSON serialization
- Implement browser-based E2E tests (8 tests) with Playwright
- Create Docker setup for repeatable E2E testing environment
- Add GitHub Actions CI workflow for automated testing
- Update .gitignore for test artifacts and temporary files

Testing Infrastructure:
- Backend: Vitest + Supertest for API integration tests
- Frontend: Vitest + Testing Library for component tests
- E2E: Playwright with Chromium for full browser automation
- CI/CD: GitHub Actions with parallel test execution

Security Improvements:
- Make dataURL size limit configurable (default: 10MB)
- Enhanced validation for image dataURLs
- Block malicious content (javascript:, script tags)

All tests pass: 41 total (22 backend + 11 frontend + 8 E2E)

* feat(tests): add comprehensive E2E tests for dashboard workflows and image persistence
chore(env): update environment variables for consistent API URL usage
fix(api): centralize API request helpers for drawing and collection management
style(DrawingCard): enhance accessibility with ARIA attributes and data-testid for testing

* Add end-to-end tests for drawing CRUD, export/import, search/sort, and theme toggle functionalities

- Implemented E2E tests for drawing creation, editing, and deletion in `drawing-crud.spec.ts`.
- Added tests for export and import features, including JSON and SQLite formats in `export-import.spec.ts`.
- Created tests for searching and sorting drawings by name and date in `search-and-sort.spec.ts`.
- Developed tests for theme toggle functionality to ensure persistence across sessions in `theme-toggle.spec.ts`.

* Update backend/src/__tests__/testUtils.ts

---------

Co-authored-by: Zimeng Xiong <zxzimeng@gmail.com>
* version bump 0.1.8

* fix(ci): consolidate E2E server startup to prevent shell isolation issues

Background processes started with & in separate GitHub Actions run steps
can terminate when those steps complete because each step creates a new
shell. This caused the backend and frontend servers to die before the
E2E tests could run.

Fixed by consolidating server startup and test execution into a single
shell step with:
- Proper PID tracking for cleanup
- Health check loops instead of fixed sleep times
- All processes run in the same shell session

* fix(ci): use absolute database path for E2E tests

* fix(backend): use resolved DATABASE_URL path for export/import endpoints

---------

Co-authored-by: Adrian Acala <adrianacala017@gmail.com>
2025-12-19 15:09:15 -08:00
Zimeng Xiong 18c8595c2e bump version 0.1.7 2025-12-01 14:09:37 -08:00
Zimeng Xiong 2e6b94644f bump version 0.1.7 2025-12-01 14:02:32 -08:00
Zimeng Xiong b0bdc05071 Merge pull request #15 from AdrianAcala/perf/drawings-optim
perf: optimize drawings endpoint with caching and lazy loading
2025-12-01 13:59:08 -08:00
Zimeng Xiong 2520d7e7a2 fix(drawings): stabilize lazy loading, improve export error handling, and tidy cache invalidation 2025-12-01 13:58:24 -08:00
Zimeng Xiong 32985ea6fe Merge pull request #13 from AdrianAcala/12-backend-url-config-fix
Add backend URL configuration for frontend and update nginx setup
2025-12-01 13:28:44 -08:00
Zimeng Xiong f8830a8b0f add example in docker-compose, clarify README, add clearer validation, longer timeouts for websocket connections 2025-12-01 13:27:31 -08:00
Adrian Acala c4352185d6 refactor: optimize drawing data handling and cache management
- Improve cache key generation using JSON.stringify for consistent formatting
- Add promise deduplication in DrawingCard to prevent redundant API calls for full drawing data
- Clear full data state when drawing ID changes to ensure fresh data loading
- Fix async cache invalidation in drawing update and collection delete endpoints
- Move cache invalidation after database operations in SQLite import endpoint
- Add HydratedDrawingData type for better type safety in drawing data management
2025-11-29 11:48:47 -08:00
Adrian Acala f9986513f8 Refactor nginx configuration and update README
- Moved BACKEND_URL configuration to the frontend service in the README.
- Added validation for the generated nginx configuration in the entrypoint script.
- Removed fallback nginx configuration copy from the Dockerfile.
- Adjusted nginx template to ensure proper header formatting.

This improves the deployment process and clarifies configuration instructions.
2025-11-29 11:29:43 -08:00
Adrian Acala 6f050aec7d perf: optimize drawings endpoint with caching and lazy loading
- Add 5s in-memory cache for /drawings responses with automatic cleanup
- Split Drawing/DrawingSummary types for efficient data fetching
- Implement lazy loading of drawing data in DrawingCard component
- Add configurable DRAWINGS_CACHE_TTL_MS and RATE_LIMIT_MAX_REQUESTS env vars
- Prevent memory leaks with periodic cleanup of cache and rate limit maps
- Add loading states and better UX for export operations
- Improve JSON parsing with error handling for malformed stored data

Benchmark results (100 drawings, cached):
- Avg latency: 6.94ms (p50: 4ms, p97.5: 8ms)
- Avg throughput: 668 req/s (peak: 1,023)
- 3k requests in 5s with 0 errors

Update .gitignore to exclude generated files, env files, and build artifacts
2025-11-29 04:30:28 +00:00
Adrian Acala 05b787bc27 Add backend URL configuration for frontend and update nginx setup
- Added BACKEND_URL environment variable to docker-compose for frontend service.
- Introduced a new entrypoint script to configure nginx with the BACKEND_URL at runtime.
- Created a template for nginx configuration to handle API and WebSocket requests dynamically.
- Updated README with instructions for configuring reverse proxy setups.

Fixes #12
2025-11-28 17:56:19 -08:00
Zimeng Xiong 971046d568 Update README 2025-11-24 15:04:52 -08:00
Zimeng Xiong 602350d2e6 Merge pull request #9 from ZimengXiong/pre-release
v0.1.6 Add export button, store library in database
2025-11-24 15:01:02 -08:00
Zimeng Xiong f20d48fea2 fix migration issues 2025-11-24 14:53:17 -08:00
Zimeng Xiong c53dc010de Merge branch '8-export-drawing' into pre-release 2025-11-24 14:43:58 -08:00
Zimeng Xiong 03e778a06f add export functionality via exportUtils 2025-11-24 14:39:38 -08:00
Zimeng Xiong fa73708d97 allow importing of libraries via URL, update db schema 2025-11-24 14:32:48 -08:00
Zimeng Xiong ee8204532d Update README.md 2025-11-23 10:23:24 -08:00
Zimeng Xiong a347403a26 Fix caution message formatting in README 2025-11-23 10:15:51 -08:00
Zimeng Xiong 8becfd87bb Merge pull request #6 from ZimengXiong/pre-release
v0.1.5 Fix security issues.
2025-11-23 10:08:42 -08:00
Zimeng Xiong 1b78597649 Merge branch 'main' into pre-release 2025-11-23 10:06:08 -08:00
Zimeng Xiong d93b6493c1 fix database import in docker 2025-11-23 09:40:00 -08:00
Zimeng Xiong d581eb3e88 fix database import, allow sqlite and db format 2025-11-23 09:22:01 -08:00
Zimeng Xiong 4728ef151c release notes 2025-11-23 09:12:36 -08:00
Zimeng Xiong eb5f54a6d0 unify version numbering 2025-11-23 08:53:36 -08:00
Zimeng Xiong c502f1c0bd add version card to settings, branch push protection 2025-11-23 08:35:36 -08:00
Zimeng Xiong 8f9ac1f9c0 add dev tag to pre release dockerhub images 2025-11-23 08:03:48 -08:00
Zimeng Xiong 0787989496 add version managment script 2025-11-23 08:02:00 -08:00
Zimeng Xiong 9bc25a3dc2 update README, release notes 2025-11-23 07:43:14 -08:00
Zimeng Xiong 3cc3fd18f4 add prerelease docker script 2025-11-23 07:30:20 -08:00
Zimeng Xiong 997fa4af03 add prisma cli to dependencies, make zod checks more permissive 2025-11-23 07:08:41 -08:00
Zimeng Xiong b864e82318 Merge branch '1-413-request-entity-too-large' into pre-release 2025-11-22 22:50:40 -08:00
Zimeng Xiong 2f22be2bd7 Merge branch 'fix-CPU-blocking' into pre-release 2025-11-22 22:48:51 -08:00
Zimeng Xiong fcfb850168 Merge branch 'fix-DoS-event-blocking' into pre-release 2025-11-22 22:44:27 -08:00
Zimeng Xiong 4a224c1f92 Merge branch 'fix-rce-via-upload' into pre-release 2025-11-22 22:43:47 -08:00
Zimeng Xiong d1d17e1288 Merge branch 'fix-xss-root-execution' into pre-release 2025-11-22 22:43:31 -08:00
Zimeng Xiong 9055661b51 make async database integrity check 2025-11-22 21:59:18 -08:00
Zimeng Xiong d25a32cdd3 Fix license badge URL in README.md 2025-11-22 21:56:14 -08:00
Zimeng Xiong 8d65404514 Fix license badge URL in README.md 2025-11-22 21:56:14 -08:00
Zimeng Xiong 1b6c32d773 Merge pull request #3 from ZimengXiong/ZimengXiong-patch-1
Create LICENSE
2025-11-22 21:54:46 -08:00
Zimeng Xiong 352bcfca29 Merge pull request #3 from ZimengXiong/ZimengXiong-patch-1
Create LICENSE
2025-11-22 21:54:46 -08:00
Zimeng Xiong 448c678ecc Merge pull request #4 from ZimengXiong/ZimengXiong-readme-license
Update license badge in README.md
2025-11-22 21:53:55 -08:00
Zimeng Xiong e980b96091 Merge pull request #4 from ZimengXiong/ZimengXiong-readme-license
Update license badge in README.md
2025-11-22 21:53:55 -08:00
Zimeng Xiong fabe0fcd54 Update license badge in README.md 2025-11-22 21:53:38 -08:00
Zimeng Xiong ef27256879 Update license badge in README.md 2025-11-22 21:53:38 -08:00
Zimeng Xiong c1da41474f Create LICENSE 2025-11-22 21:51:20 -08:00
Zimeng Xiong 815dcd5c80 Create LICENSE 2025-11-22 21:51:20 -08:00
Zimeng Xiong 29936417fc convert all sync op to async, implemented streaming 2025-11-22 21:36:02 -08:00
Zimeng Xiong 49e32f7d96 validate SQlite magic header 2025-11-22 21:27:34 -08:00
Zimeng Xiong cd9c242983 filter with dompurify 2025-11-22 21:21:28 -08:00
Zimeng Xiong 3835557e67 update nginx config 2025-11-22 21:06:01 -08:00
Zimeng Xiong 69bffab745 fix XSS and Root execution of NPM in docker 2025-11-22 20:38:40 -08:00
Zimeng Xiong ef412a3887 Merge pull request #2 from ZimengXiong/fix-bind-mount-prisma
fix bind mount prisma, auto hydrate empty folder
2025-11-22 20:25:44 -08:00
Zimeng Xiong 2e2b4ca455 fix bind mount prisma, auto hydrate empty folder 2025-11-22 20:25:07 -08:00
Zimeng Xiong fb5fe1235c add fallback for browsers that do not have crypto.randomUUID 2025-11-22 19:18:05 -08:00
Zimeng Xiong e21cdbe6a8 add CORS fallback 2025-11-22 19:14:55 -08:00
Zimeng Xiong 94f33f0a56 fix: add linux-musl-openssl-3.0.x 2025-11-22 19:07:28 -08:00
Zimeng Xiong 5d5e22c8a1 fix: pinning CORS to FRONTEND_URL, validate drawing payloads with Zod, staging SQLite imports with integrity checks and atomic swaps in index.ts 2025-11-22 17:17:50 -08:00
Zimeng Xiong b3dbcc2376 Update caution note in README
Added cautionary note about security and production use.
2025-11-22 16:33:23 -08:00
112 changed files with 11810 additions and 14461 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:*)'
+199
View File
@@ -0,0 +1,199 @@
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
backend-tests:
name: Backend Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: backend/package-lock.json
- name: Install backend dependencies
run: |
cd backend
npm ci
- name: Generate Prisma client
run: |
cd backend
npx prisma generate
- name: Run backend tests
run: |
cd backend
npm test
frontend-unit-tests:
name: Frontend Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: |
cd frontend
npm ci
- name: Run frontend tests
run: |
cd frontend
npm test
e2e-tests:
name: E2E Browser Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install backend dependencies
run: |
cd backend
npm ci
- name: Generate Prisma client
run: |
cd backend
npx prisma generate
- name: Setup backend database
run: |
cd backend
npx prisma db push
env:
DATABASE_URL: file:${{ github.workspace }}/backend/prisma/e2e-test.db
- name: Install frontend dependencies
run: |
cd frontend
npm ci
- name: Install E2E test dependencies
run: |
cd e2e
npm ci
- name: Install Playwright browsers
run: |
cd e2e
npx playwright install chromium --with-deps
- name: Start servers and run E2E tests
run: |
# Start backend server in background
cd backend
DATABASE_URL="file:${{ github.workspace }}/backend/prisma/e2e-test.db" FRONTEND_URL="http://localhost:5173" npm run dev &
BACKEND_PID=$!
cd ..
# Wait for backend to be ready
echo "Waiting for backend server..."
for i in {1..30}; do
if curl -s http://localhost:8000/health > /dev/null; then
echo "Backend is ready!"
break
fi
echo "Attempt $i: Backend not ready yet..."
sleep 2
done
# Start frontend server in background
cd frontend
npm run dev -- --host &
FRONTEND_PID=$!
cd ..
# Wait for frontend to be ready
echo "Waiting for frontend server..."
for i in {1..30}; do
if curl -s http://localhost:5173 > /dev/null; then
echo "Frontend is ready!"
break
fi
echo "Attempt $i: Frontend not ready yet..."
sleep 2
done
# Run E2E tests
cd e2e
NO_SERVER=true CI=true npx playwright test
TEST_EXIT_CODE=$?
# Cleanup
kill $BACKEND_PID $FRONTEND_PID 2>/dev/null || true
exit $TEST_EXIT_CODE
env:
DATABASE_URL: file:${{ github.workspace }}/backend/prisma/e2e-test.db
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: e2e/playwright-report/
retention-days: 7
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
path: e2e/test-results/
retention-days: 7
# Security tests for data sanitization
security-tests:
name: Security Sanitization Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: backend/package-lock.json
- name: Install backend dependencies
run: |
cd backend
npm ci
- name: Generate Prisma client
run: |
cd backend
npx prisma generate
- name: Run security tests
run: |
cd backend
npx ts-node src/securityTest.ts
+110 -1
View File
@@ -1,3 +1,112 @@
# Dependencies
frontend/node_modules
.DS_Store
backend/node_modules
# Database
backend/prisma/*.db
backend/prisma/**/*.db
backend/prisma/*.db-journal
backend/prisma/**/*.db-journal
backend/prisma/dev.db
backend/prisma/e2e-test.db
backend/prisma/*.backup
# Uploads
backend/uploads/
# Generated files
backend/src/generated/
# Environment variables
.env
.env.local
.env.production
.env.staging
# Build outputs
frontend/dist/
frontend/build/
backend/dist/
# E2E Testing
e2e/node_modules/
e2e/test-results/
e2e/test-results-user/
e2e/playwright-report/
e2e/playwright-report-user/
e2e/.playwright/
# Temporary files
*.tmp
*.temp
*.bak
# Test artifacts (in case they appear in other locations)
**/playwright-report/
**/test-results/
**/playwright/.cache/
# Docker volumes (if any temporary ones are created)
docker-volumes/
# Logs
*.log
logs/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# Vitest cache
.vitest/
# Playwright screenshots/videos on failure
**/screenshots/
**/videos/
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env.test
# IDE/Editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
*.tmp
*.temp
+1 -1
View File
@@ -148,7 +148,7 @@ ExcaliDash/
**Backend (.env):**
```bash
DATABASE_URL="file:./prisma/dev.db"
DATABASE_URL="file:./dev.db"
PORT=8000
NODE_ENV=development
```
+661
View File
File diff suppressed because it is too large Load Diff
+550
View File
File diff suppressed because it is too large Load Diff
+47 -3
View File
@@ -1,8 +1,9 @@
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
# ExcaliDash v0.1.0
# ExcaliDash
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
![License](https://img.shields.io/github/license/zimengxiong/ExcaliDash)
![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)
[![Docker](https://img.shields.io/badge/docker-ready-blue.svg)](https://hub.docker.com)
A self-hosted dashboard and organizer for [Excalidraw](https://github.com/excalidraw/excalidraw) with live collaboration features.
@@ -21,6 +22,8 @@ A self-hosted dashboard and organizer for [Excalidraw](https://github.com/excali
- [Installation](#installation)
- [Docker Hub (Recommended)](#dockerhub-recommended)
- [Docker Build](#docker-build)
- [Reverse Proxy / Traefik Setups](#reverse-proxy--traefik-setups-docker)
- [Multi-Container / Kubernetes Deployments](#multi-container--kubernetes-deployments)
- [Development](#development)
- [Clone the Repository](#clone-the-repository)
- [Frontend](#frontend)
@@ -74,7 +77,10 @@ See [release notes](https://github.com/ZimengXiong/ExcaliDash/releases) for a sp
# Installation
> [!CAUTION]
> NOT for production use. This is just a side project (and also the first release), and it likely contains some bugs. DO NOT open ports to the internet (e.g. CORS is set to allow all)
> NOT for production use. While attempts have been made at hardening (XSS/dompurify, CORS, rate-limiting, sanitization), they are inadequate for public deployment. Do not expose any ports.
> [!CAUTION]
> ExcaliDash is in BETA. Please backup your data regularly (e.g. with cron).
## Docker Hub (Recommended)
@@ -110,6 +116,44 @@ docker compose up -d
# Access the frontend at localhost:6767
```
### Reverse Proxy / Traefik Setups (Docker)
When running ExcaliDash behind Traefik, Nginx, or another reverse proxy, configure both containers so that API + WebSocket calls resolve correctly:
- `FRONTEND_URL` (backend) must match the public URL that users hit (e.g. `https://excalidash.example.com`). This controls CORS and Socket.IO origin checks.
- `BACKEND_URL` (frontend) tells the Nginx container how to reach the backend from inside Docker/Kubernetes. Override it if your reverse proxy exposes the backend under a different hostname.
```yaml
# docker-compose.yml example
backend:
environment:
- FRONTEND_URL=https://excalidash.example.com
frontend:
environment:
# For standard Docker Compose (default)
# - BACKEND_URL=backend:8000
# For Kubernetes, use the service DNS name:
- BACKEND_URL=excalidash-backend.default.svc.cluster.local:8000
```
### Multi-Container / Kubernetes Deployments
When running multiple backend replicas (e.g., Kubernetes, Docker Swarm, or load-balanced containers), you **must** set the `CSRF_SECRET` environment variable to the same value across all instances.
```bash
# Generate a secure secret
openssl rand -base64 32
```
```yaml
# docker-compose.yml or k8s deployment
backend:
environment:
- CSRF_SECRET=your-generated-secret-here
```
Without this, each container generates its own ephemeral CSRF secret, causing token validation failures when requests are routed to different replicas. Single-container deployments work without this setting.
# Development
## Clone the Repository
+43
View File
@@ -0,0 +1,43 @@
CSRF Protection (8a78b2b)
- Implemented comprehensive CSRF (Cross-Site Request Forgery) protection for enhanced security
- Added new backend/src/security.ts module for security utilities
- Frontend API layer now handles CSRF tokens automatically
- Added integration tests for CSRF validation
Upload Progress Indicator (8f9b9b4)
- Added a visual upload progress bar when users upload files
- New UploadContext for managing upload state across components
- New UploadStatus component displaying real-time upload progress
- Save status indicator when navigating back from the editor
- Improved error handling and recovery for failed uploads
Bug Fixes
- Fixed broken e2e tests (cae8f3c)
- Replaced deprecated substr() with substring()
- Fixed stale state issues in error handling
- Fixed missing useEffect dependencies
- Fixed CSS class conflicts in progress bar styling
- Added error recovery for save state in Editor
Infrastructure
- 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.
```
-202
View File
@@ -1,202 +0,0 @@
# Security Fixes Implementation Summary
## Overview
This document summarizes the comprehensive security fixes implemented to address two critical security vulnerabilities identified in ExcaliDash:
1. **Stored XSS Vector (High Severity)** - Data sanitization negligence
2. **Root Execution Privilege (Critical Severity)** - Container escape risk
## Security Issues Fixed
### Issue 1: Stored XSS Vector (High Severity) ✅ FIXED
**Problem**: Backend used lazy `z.object({}).passthrough()` validation for elements and appState, allowing arbitrary JSON storage without sanitization.
**Attack Vectors**:
- Malicious `.excalidraw` files containing `<script>` tags in element properties
- `javascript:` URIs in link attributes
- SVG previews with embedded malicious code
- Compromised clients sending XSS payloads
**Solution Implemented**:
- **Strict Zod Schemas**: Replaced `.passthrough()` with detailed validation schemas for elements and appState
- **HTML/JS Sanitization**: Implemented comprehensive sanitization layer removing script tags, event handlers, and malicious URLs
- **SVG Sanitization**: Special handling for SVG content to prevent script execution
- **URL Validation**: Whitelist-only approach for URL schemes (http, https, mailto, relative paths only)
- **Input Sanitization**: All string inputs are sanitized before database persistence
- **Import Validation**: Additional security checks for imported .excalidraw files with `X-Imported-File` header
### Issue 2: Root Execution Privilege (Critical Severity) ✅ FIXED
**Problem**: Container ran Node.js process as root without USER directive, providing immediate root access in case of RCE.
**Attack Vectors**:
- RCE vulnerabilities in `better-sqlite3` native bindings
- File upload processing vulnerabilities
- Import functionality exploits
**Solution Implemented**:
- **Non-Root User**: Created dedicated `nodejs` user with UID 1001
- **Permission Management**: Proper ownership and permissions for data directories
- **Dockerfile Security**: Added USER directive to switch to non-root execution
- **Entry Point Security**: Updated docker-entrypoint.sh to handle permissions correctly
### Additional Security Hardening ✅ IMPLEMENTED
**Security Headers**:
- Content Security Policy (CSP) with strict source restrictions
- X-Frame-Options: DENY (prevents clickjacking)
- X-Content-Type-Options: nosniff
- X-XSS-Protection: 1; mode=block
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: geolocation=(), microphone=(), camera=()
**Rate Limiting**:
- Implemented basic rate limiting (1000 requests per 15-minute window)
- Per-IP tracking to prevent DoS attacks
**Request Validation**:
- Maintained existing 50MB request size limits
- Enhanced validation for file imports
## Files Modified
### Backend Changes
1. **`backend/src/security.ts`** - New security utilities module
- HTML/JS sanitization functions
- SVG sanitization functions
- Strict Zod schemas for elements and appState
- Drawing data validation and sanitization
- URL sanitization with whitelist validation
2. **`backend/src/index.ts`** - Updated backend security
- Replaced lazy `.passthrough()` schemas with strict validation
- Added security middleware with headers and rate limiting
- Enhanced POST /drawings endpoint with import validation
- Added malicious content detection and rejection
3. **`backend/Dockerfile`** - Container security hardening
- Created non-root `nodejs` user (UID 1001)
- Added USER directive for non-root execution
- Set proper file ownership and permissions
4. **`backend/docker-entrypoint.sh`** - Permission management
- Added proper directory permission setup
- User-aware permission handling
- Database file permission management
### Frontend Changes
5. **`frontend/src/utils/importUtils.ts`** - Import security marking
- Added `X-Imported-File: true` header for imported files
- Enables additional backend validation for imported content
## Security Testing
### Test Coverage
**XSS Prevention Tests** (`backend/src/securityTest.ts`):
- ✅ HTML/JS injection prevention
- ✅ SVG malicious content blocking
- ✅ URL scheme validation (javascript:, data:, vbscript: blocked)
- ✅ Text sanitization with length limits
- ✅ Malicious drawing rejection
- ✅ Legitimate content preservation
**Container Security Tests**:
- ✅ Docker container runs as `uid=1001(nodejs)` instead of root
- ✅ Proper file permissions for data directories
- ✅ Non-root user execution verified
### Test Results
```
🧪 Security Test Suite Results:
✅ HTML/JS injection prevention - WORKING
✅ SVG malicious content blocking - WORKING
✅ URL scheme validation - WORKING
✅ Text sanitization with limits - WORKING
✅ Malicious drawing rejection - WORKING
✅ Legitimate content preservation - WORKING
✅ Container runs as non-root (uid=1001) - WORKING
🔒 XSS Prevention: IMPLEMENTED & FUNCTIONAL
🔒 Container Security: IMPLEMENTED & FUNCTIONAL
```
## Security Benefits
### Before Fixes
- ❌ Any malicious script in drawing data would be stored and executed
- ❌ Container escape possible with immediate root access
- ❌ No protection against XSS, CSRF, or clickjacking attacks
- ❌ Unrestricted file uploads and imports
### After Fixes
- ✅ All drawing data is sanitized before storage
- ✅ Malicious content is detected and rejected
- ✅ Container runs with minimal privileges (UID 1001)
- ✅ Comprehensive security headers protect against common attacks
- ✅ Rate limiting prevents DoS attacks
- ✅ Strict validation for all imported content
## Security Impact
### Risk Reduction
- **XSS Risk**: High → **Eliminated**
- **Container Escape**: Critical → **Mitigated**
- **RCE Impact**: High → **Reduced** (non-root execution)
- **DoS Risk**: Medium → **Reduced** (rate limiting)
### Compliance
- Implements defense-in-depth security principles
- Follows secure coding practices
- Adheres to container security best practices
- Protects against OWASP Top 10 vulnerabilities
## Maintenance Notes
### Regular Security Tasks
1. **Security Test Suite**: Run `npm run security-test` to verify XSS prevention
2. **Container Security**: Verify non-root execution in production
3. **Dependency Updates**: Keep dependencies updated for security patches
4. **Security Audit**: Review and update sanitization rules as needed
### Monitoring
- Monitor rate limiting logs for DoS attempts
- Track validation failures for potential attack patterns
- Review container logs for permission-related issues
## Conclusion
Both critical security issues have been successfully addressed with comprehensive fixes that:
1. **Eliminate XSS vulnerabilities** through strict validation and sanitization
2. **Reduce container escape risk** through non-root execution
3. **Add defense-in-depth** security measures
4. **Maintain full functionality** while improving security posture
The implementation includes thorough testing to ensure security measures work correctly while preserving legitimate functionality.
**Security Status**: ✅ **RESOLVED**
+1
View File
@@ -0,0 +1 @@
0.3.0
+1
View File
@@ -2,3 +2,4 @@
PORT=8000
NODE_ENV=production
DATABASE_URL=file:/app/prisma/dev.db
FRONTEND_URL=http://localhost:6767
+6 -9
View File
@@ -25,8 +25,8 @@ RUN npx tsc
# Production stage
FROM node:20-alpine
# Install OpenSSL for Prisma and create non-root user
RUN apk add --no-cache openssl && \
# Install OpenSSL for Prisma and su-exec, create non-root user
RUN apk add --no-cache openssl su-exec && \
addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
@@ -51,17 +51,14 @@ COPY --from=builder /app/src/generated ./dist/generated
# Generate Prisma Client in production (updates node_modules)
RUN npx prisma generate
# Create necessary directories and set proper ownership
RUN mkdir -p /app/uploads /app/prisma && \
chown -R nodejs:nodejs /app
# Create necessary directories (ownership will be set in entrypoint)
RUN mkdir -p /app/uploads /app/prisma
# Copy and set permissions for entrypoint script
COPY docker-entrypoint.sh ./
RUN chmod +x docker-entrypoint.sh && \
chown nodejs:nodejs docker-entrypoint.sh
RUN chmod +x docker-entrypoint.sh
# Switch to non-root user
USER nodejs
# REMOVED: USER nodejs (We must stay root to fix permissions in entrypoint)
EXPOSE 8000
+21 -23
View File
@@ -1,36 +1,34 @@
#!/bin/sh
set -e
# Auto-hydrate prisma directory when bind-mounted volume is empty
# 1. Hydrate volume if empty (Running as root)
if [ ! -f "/app/prisma/schema.prisma" ]; then
echo "Mount is empty. Hydrating /app/prisma from /app/prisma_template..."
cp -R /app/prisma_template/. /app/prisma/
echo "Mount is empty. Hydrating /app/prisma..."
cp -R /app/prisma_template/. /app/prisma/
else
# Volume exists but may be missing new migrations from an upgrade
# Always sync schema and migrations from template to ensure upgrades work
echo "Syncing schema and migrations from template..."
cp /app/prisma_template/schema.prisma /app/prisma/schema.prisma
cp -R /app/prisma_template/migrations/. /app/prisma/migrations/
fi
# Ensure proper ownership and permissions for data directories
echo "Setting up data directory permissions..."
mkdir -p /app/uploads
mkdir -p /app/prisma
# Set ownership to the node user (UID 1000)
if [ "$(id -u)" = "0" ]; then
# If running as root (for some reason), fix ownership
chown -R nodejs:nodejs /app/uploads
chown -R nodejs:nodejs /app/prisma
fi
# 2. Fix permissions unconditionally (Running as root)
echo "Fixing filesystem permissions..."
chown -R nodejs:nodejs /app/uploads
chown -R nodejs:nodejs /app/prisma
chmod 755 /app/uploads
# Ensure database file has proper permissions
if [ -f "/app/prisma/dev.db" ]; then
chmod 664 /app/prisma/dev.db 2>/dev/null || true
echo "Database file found, ensuring write permissions..."
chmod 666 /app/prisma/dev.db
fi
# Set appropriate permissions for uploads directory
chmod 755 /app/uploads
# Run migrations as the current user
# 3. Run Migrations (Drop privileges to nodejs)
echo "Running database migrations..."
npx prisma migrate deploy
su-exec nodejs npx prisma migrate deploy
# Start the application
echo "Starting application as user $(whoami) (UID: $(id -u))"
node dist/index.js
# 4. Start Application (Drop privileges to nodejs)
echo "Starting application as nodejs..."
exec su-exec nodejs node dist/index.js
+1901 -351
View File
File diff suppressed because it is too large Load Diff
+11 -6
View File
@@ -1,11 +1,13 @@
{
"name": "backend",
"version": "1.0.0",
"version": "0.3.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"keywords": [],
"author": "",
@@ -14,7 +16,7 @@
"dependencies": {
"@prisma/client": "^5.22.0",
"@types/archiver": "^7.0.0",
"@types/jsdom": "^27.0.0",
"@types/jsdom": "^21.1.7",
"@types/multer": "^2.0.0",
"@types/socket.io": "^3.0.1",
"archiver": "^7.0.1",
@@ -23,8 +25,9 @@
"dompurify": "^3.3.0",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"jsdom": "^27.2.0",
"jsdom": "^22.1.0",
"multer": "^2.0.2",
"prisma": "^5.22.0",
"socket.io": "^4.8.1",
"zod": "^4.1.12"
},
@@ -32,9 +35,11 @@
"@types/cors": "^2.8.19",
"@types/express": "^5.0.5",
"@types/node": "^24.10.1",
"@types/supertest": "^6.0.3",
"nodemon": "^3.1.11",
"prisma": "^5.22.0",
"supertest": "^7.1.4",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
"typescript": "^5.9.3",
"vitest": "^4.0.15"
}
}
Binary file not shown.
@@ -0,0 +1,7 @@
-- CreateTable
CREATE TABLE "Library" (
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'default',
"items" TEXT NOT NULL DEFAULT '[]',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
Binary file not shown.
+7
View File
@@ -33,3 +33,10 @@ model Drawing {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Library {
id String @id @default("default") // Singleton pattern - use "default" ID
items String @default("[]") // Stored as JSON string array of library items
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+168
View File
@@ -0,0 +1,168 @@
/**
* CSRF Tests - Horizontal Scaling (K8s) Validation
*
* PR #20 review concern:
* "Worried that in memory token store might not work on horizontal scaling"
*
* Fix:
* - CSRF tokens are now stateless and HMAC-signed using a shared `CSRF_SECRET`.
* - Any pod can validate any token as long as all pods share the same secret.
*
* These tests prove:
* - Tokens validate correctly for the issuing client id
* - Tokens do NOT validate for a different client id
* - Tokens expire after 24 hours
* - Tokens validate across separate module instances (simulated pods)
*/
import { describe, it, expect, beforeAll, afterEach, vi } from "vitest";
const SHARED_SECRET = "test-shared-csrf-secret";
beforeAll(() => {
// Must be shared across instances/pods for horizontal scaling.
process.env.CSRF_SECRET = SHARED_SECRET;
});
afterEach(() => {
vi.useRealTimers();
});
describe("CSRF - stateless HMAC tokens", () => {
it("creates a token in payload.signature format and validates for same client id", async () => {
const { createCsrfToken, validateCsrfToken } = await import("../security");
const clientId = "test-client-1";
const token = createCsrfToken(clientId);
expect(typeof token).toBe("string");
// base64url(payload).base64url(signature)
expect(token).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/);
expect(validateCsrfToken(clientId, token)).toBe(true);
});
it("rejects validation for a different client id (token binding)", async () => {
const { createCsrfToken, validateCsrfToken } = await import("../security");
const token = createCsrfToken("client-a");
expect(validateCsrfToken("client-b", token)).toBe(false);
});
it("rejects malformed tokens", async () => {
const { validateCsrfToken } = await import("../security");
expect(validateCsrfToken("client", "not-a-token")).toBe(false);
expect(validateCsrfToken("client", "a.b.c")).toBe(false);
expect(validateCsrfToken("client", "")).toBe(false);
});
it("revokeCsrfToken is a no-op for stateless tokens (does not break callers)", async () => {
const { createCsrfToken, validateCsrfToken, revokeCsrfToken } = await import(
"../security"
);
const clientId = "client-revoke";
const token = createCsrfToken(clientId);
expect(validateCsrfToken(clientId, token)).toBe(true);
revokeCsrfToken(clientId);
// Stateless token remains valid until expiry
expect(validateCsrfToken(clientId, token)).toBe(true);
});
it("expires tokens after 24 hours", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2025-01-01T00:00:00.000Z"));
const { createCsrfToken, validateCsrfToken } = await import("../security");
const clientId = "client-expiry";
const token = createCsrfToken(clientId);
expect(validateCsrfToken(clientId, token)).toBe(true);
// 24h + 1ms later
vi.setSystemTime(new Date("2025-01-02T00:00:00.001Z"));
expect(validateCsrfToken(clientId, token)).toBe(false);
});
});
describe("CSRF - horizontal scaling (simulated pods)", () => {
it("validates across module instances (pod A issues, pod B validates)", async () => {
const clientId = "user-123";
vi.resetModules();
const podA = await import("../security");
const token = podA.createCsrfToken(clientId);
// Simulate a different pod (new Node.js process / fresh module state)
vi.resetModules();
const podB = await import("../security");
expect(podB.validateCsrfToken(clientId, token)).toBe(true);
});
it("has 0% failure rate under round-robin validation across 3 pods", async () => {
const clientId = "user-round-robin";
const pods: Array<{
createCsrfToken: (clientId: string) => string;
validateCsrfToken: (clientId: string, token: string) => boolean;
}> = [];
for (let i = 0; i < 3; i++) {
vi.resetModules();
pods.push(await import("../security"));
}
// Token issued on one pod
const token = pods[0].createCsrfToken(clientId);
// Validate on alternating pods (simulates a non-sticky load balancer)
const attempts = 60;
let failures = 0;
for (let i = 0; i < attempts; i++) {
const pod = pods[i % pods.length];
if (!pod.validateCsrfToken(clientId, token)) failures++;
}
expect(failures).toBe(0);
});
});
describe("CSRF - referer origin parsing", () => {
it("extracts exact origin from a referer URL", async () => {
const { getOriginFromReferer } = await import("../security");
expect(getOriginFromReferer("https://example.com/path?x=1")).toBe(
"https://example.com"
);
expect(getOriginFromReferer("http://localhost:5173/some/page")).toBe(
"http://localhost:5173"
);
});
it("does not allow prefix tricks (origin must be parsed)", async () => {
const { getOriginFromReferer } = await import("../security");
expect(
getOriginFromReferer("https://example.com.evil.com/anything")
).toBe("https://example.com.evil.com");
// `startsWith("https://example.com")` would incorrectly allow this.
expect(getOriginFromReferer("https://example.com@evil.com/anything")).toBe(
"https://evil.com"
);
});
it("returns null for invalid or non-http(s) referers", async () => {
const { getOriginFromReferer } = await import("../security");
expect(getOriginFromReferer("")).toBeNull();
expect(getOriginFromReferer("not a url")).toBeNull();
expect(getOriginFromReferer("file:///etc/passwd")).toBeNull();
expect(getOriginFromReferer(null)).toBeNull();
});
});
File diff suppressed because it is too large Load Diff
+218
View File
@@ -0,0 +1,218 @@
/**
* Test utilities for backend integration tests
*/
import { PrismaClient } from "../generated/client";
import path from "path";
import { execSync } from "child_process";
// Use a separate test database
const TEST_DB_PATH = path.resolve(__dirname, "../../prisma/test.db");
/**
* Get a test Prisma client pointing to the test database
*/
export const getTestPrisma = () => {
const databaseUrl = `file:${TEST_DB_PATH}`;
process.env.DATABASE_URL = databaseUrl;
return new PrismaClient({
datasources: {
db: {
url: databaseUrl,
},
},
});
};
/**
* Setup the test database by running migrations
*/
export const setupTestDb = () => {
const databaseUrl = `file:${TEST_DB_PATH}`;
process.env.DATABASE_URL = databaseUrl;
// Run Prisma migrations to create the test database
try {
execSync("npx prisma db push --skip-generate", {
cwd: path.resolve(__dirname, "../../"),
env: { ...process.env, DATABASE_URL: databaseUrl },
stdio: "pipe",
});
} catch (error) {
console.error("Failed to setup test database:", error);
throw error;
}
};
/**
* Clean up the test database between tests
*/
export const cleanupTestDb = async (prisma: PrismaClient) => {
// Delete all drawings and collections (except Trash)
await prisma.drawing.deleteMany({});
await prisma.collection.deleteMany({
where: { id: { not: "trash" } },
});
};
/**
* Initialize test database with required data
*/
export const initTestDb = async (prisma: PrismaClient) => {
// Ensure Trash collection exists
const trash = await prisma.collection.findUnique({
where: { id: "trash" },
});
if (!trash) {
await prisma.collection.create({
data: { id: "trash", name: "Trash" },
});
}
};
/**
* Generate a sample base64 PNG image data URL
* This creates a small but valid PNG for testing
*/
export const generateSampleImageDataUrl = (size: "small" | "medium" | "large" = "small"): string => {
// Minimal 1x1 red PNG (smallest valid PNG possible)
const smallPng = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
if (size === "small") {
return `data:image/png;base64,${smallPng}`;
}
// For medium/large, repeat the pattern to create larger payloads
const repetitions = size === "medium" ? 1000 : 10000;
const paddedBase64 = smallPng.repeat(repetitions);
return `data:image/png;base64,${paddedBase64}`;
};
/**
* Generate a large image data URL that exceeds the 10000 char limit
* This is specifically designed to catch the truncation bug from issue #17
*/
export const generateLargeImageDataUrl = (): string => {
// Create a base64 string that's definitely larger than 10000 characters
// This simulates a real image that would get truncated by the old code
const baseImage = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
// Repeat to create a ~50KB payload
const largeBase64 = baseImage.repeat(500);
return `data:image/png;base64,${largeBase64}`;
};
/**
* Create a sample Excalidraw files object with embedded images
*/
export const createSampleFilesObject = (imageCount: number = 1, size: "small" | "large" = "small") => {
const files: Record<string, any> = {};
for (let i = 0; i < imageCount; i++) {
const fileId = `file-${i}-${Date.now()}`;
files[fileId] = {
id: fileId,
mimeType: "image/png",
dataURL: size === "large" ? generateLargeImageDataUrl() : generateSampleImageDataUrl("small"),
created: Date.now(),
lastRetrieved: Date.now(),
};
}
return files;
};
/**
* Create a minimal valid Excalidraw drawing payload
*/
export const createTestDrawingPayload = (options: {
name?: string;
files?: Record<string, any> | null;
elements?: any[];
appState?: any;
} = {}) => {
return {
name: options.name ?? "Test Drawing",
elements: options.elements ?? [
{
id: "element-1",
type: "rectangle",
x: 100,
y: 100,
width: 200,
height: 100,
angle: 0,
strokeColor: "#000000",
backgroundColor: "transparent",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
groupIds: [],
frameId: null,
roundness: null,
seed: 12345,
version: 1,
versionNonce: 1,
isDeleted: false,
boundElements: null,
updated: Date.now(),
link: null,
locked: false,
},
],
appState: options.appState ?? {
viewBackgroundColor: "#ffffff",
gridSize: null,
},
files: options.files ?? null,
preview: null,
collectionId: null,
};
};
/**
* Compare two files objects to check if image data was preserved
*/
export const compareFilesObjects = (original: Record<string, any>, received: Record<string, any>): {
isEqual: boolean;
differences: string[];
} => {
const differences: string[] = [];
const originalKeys = Object.keys(original);
const receivedKeys = Object.keys(received);
if (originalKeys.length !== receivedKeys.length) {
differences.push(`Key count mismatch: original=${originalKeys.length}, received=${receivedKeys.length}`);
}
for (const key of originalKeys) {
if (!(key in received)) {
differences.push(`Missing key: ${key}`);
continue;
}
const origFile = original[key];
const recvFile = received[key];
// Check dataURL specifically - this is where truncation would occur
if (origFile.dataURL !== recvFile.dataURL) {
differences.push(
`DataURL mismatch for ${key}: ` +
`original length=${origFile.dataURL?.length ?? 0}, ` +
`received length=${recvFile.dataURL?.length ?? 0}`
);
// Check if it was truncated
if (recvFile.dataURL && origFile.dataURL?.startsWith(recvFile.dataURL.substring(0, 100))) {
differences.push(`TRUNCATION DETECTED: dataURL was cut short`);
}
}
}
return {
isEqual: differences.length === 0,
differences,
};
};
-29
View File
@@ -1,29 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma-related types and utilities in a browser.
* Use it to get access to models, enums, and input types.
*
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
* See `client.ts` for the standard, server-side entry point.
*
* 🟢 You can import this file directly.
*/
import * as Prisma from './internal/prismaNamespaceBrowser'
export { Prisma }
export * as $Enums from './enums'
export * from './enums';
/**
* Model Collection
*
*/
export type Collection = Prisma.CollectionModel
/**
* Model Drawing
*
*/
export type Drawing = Prisma.DrawingModel
-49
View File
@@ -1,49 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
*
* 🟢 You can import this file directly.
*/
import * as process from 'node:process'
import * as path from 'node:path'
import * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums"
import * as $Class from "./internal/class"
import * as Prisma from "./internal/prismaNamespace"
export * as $Enums from './enums'
export * from "./enums"
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Collections
* const collections = await prisma.collection.findMany()
* ```
*
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
*/
export const PrismaClient = $Class.getPrismaClientClass()
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma }
/**
* Model Collection
*
*/
export type Collection = Prisma.CollectionModel
/**
* Model Drawing
*
*/
export type Drawing = Prisma.DrawingModel

Some files were not shown because too many files have changed in this diff Show More