Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44fb456405 | |||
| 8f9b9b4945 | |||
| cae8f3cbf6 | |||
| e4e48b13d8 | |||
| 8a78b2bb2e | |||
| e75b727a5a | |||
| c2aa742a79 | |||
| 49b413bf07 | |||
| 18c8595c2e | |||
| 2e6b94644f | |||
| b0bdc05071 | |||
| 2520d7e7a2 | |||
| 32985ea6fe | |||
| f8830a8b0f | |||
| c4352185d6 | |||
| f9986513f8 | |||
| 6f050aec7d | |||
| 05b787bc27 |
@@ -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
@@ -1,3 +1,112 @@
|
|||||||
|
# Dependencies
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
|
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
|
||||||
backend/prisma/*.db
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
@@ -148,7 +148,7 @@ ExcaliDash/
|
|||||||
**Backend (.env):**
|
**Backend (.env):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DATABASE_URL="file:./prisma/dev.db"
|
DATABASE_URL="file:./dev.db"
|
||||||
PORT=8000
|
PORT=8000
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
|
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
|
||||||
|
|
||||||
# ExcaliDash v0.1.6
|
# ExcaliDash v0.1.8
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -22,6 +22,8 @@ A self-hosted dashboard and organizer for [Excalidraw](https://github.com/excali
|
|||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Docker Hub (Recommended)](#dockerhub-recommended)
|
- [Docker Hub (Recommended)](#dockerhub-recommended)
|
||||||
- [Docker Build](#docker-build)
|
- [Docker Build](#docker-build)
|
||||||
|
- [Reverse Proxy / Traefik Setups](#reverse-proxy--traefik-setups-docker)
|
||||||
|
- [Multi-Container / Kubernetes Deployments](#multi-container--kubernetes-deployments)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Clone the Repository](#clone-the-repository)
|
- [Clone the Repository](#clone-the-repository)
|
||||||
- [Frontend](#frontend)
|
- [Frontend](#frontend)
|
||||||
@@ -75,7 +77,7 @@ See [release notes](https://github.com/ZimengXiong/ExcaliDash/releases) for a sp
|
|||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> 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. Currently lacking CSRF.
|
> 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]
|
> [!CAUTION]
|
||||||
> ExcaliDash is in BETA. Please backup your data regularly (e.g. with cron).
|
> ExcaliDash is in BETA. Please backup your data regularly (e.g. with cron).
|
||||||
@@ -114,6 +116,44 @@ docker compose up -d
|
|||||||
# Access the frontend at localhost:6767
|
# 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
|
# Development
|
||||||
|
|
||||||
## Clone the Repository
|
## Clone the Repository
|
||||||
|
|||||||
+22
-23
@@ -1,30 +1,29 @@
|
|||||||
# ExcaliDash v0.1.5
|
CSRF Protection (8a78b2b)
|
||||||
|
|
||||||
Date: 2025-11-23
|
- 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
|
||||||
|
|
||||||
Compatibility: v0.1.x (Backward Compatible)
|
Upload Progress Indicator (8f9b9b4)
|
||||||
|
|
||||||
# Security
|
- 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
|
||||||
|
|
||||||
- RCE: implemented strict Zod schema validation and input sanitization on file uploads; added path traversal guards to file handling logic
|
Bug Fixes
|
||||||
|
|
||||||
- XSS: used DOMPurify for HTML sanitization; blocked execution-capable SVG attributes and enforces CSP headers.
|
- 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
|
||||||
|
|
||||||
- DoS: moved CPU-intensive operations to worker threads to prevent event loop blocking; request rate limiting (1,000 req/15 min per IP) and streaming for large files
|
Infrastructure
|
||||||
|
|
||||||
# Infras & Deployment
|
- Updated docker-compose configurations with new environment variables
|
||||||
|
- E2E test suite improvements and reliability fixes
|
||||||
- non-root execution (uid 1001) in containers
|
- Added Kubernetes deployment note in README
|
||||||
- migrated to multi-stage Docker builds
|
|
||||||
|
|
||||||
# Database
|
|
||||||
|
|
||||||
- migrated to better-sqlite3, converted all DB interactions to non-blocking async operations and offloaded integrity checks to worker threads.
|
|
||||||
|
|
||||||
- implemented SQLite magic header validation; added automatic backup triggers preceding data import
|
|
||||||
|
|
||||||
- input validation logic
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
|
|
||||||
- updated Settings UI to show version
|
|
||||||
|
|||||||
+2
-5
@@ -8,10 +8,7 @@ COPY package*.json ./
|
|||||||
COPY tsconfig.json ./
|
COPY tsconfig.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
# Install build deps required for compiling native modules like better-sqlite3
|
RUN npm ci
|
||||||
RUN apk add --no-cache python3 make g++ build-base sqlite-dev && \
|
|
||||||
npm ci
|
|
||||||
ENV PYTHON=/usr/bin/python3
|
|
||||||
|
|
||||||
# Copy prisma schema
|
# Copy prisma schema
|
||||||
COPY prisma ./prisma/
|
COPY prisma ./prisma/
|
||||||
@@ -29,7 +26,7 @@ RUN npx tsc
|
|||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
# Install OpenSSL for Prisma and su-exec, create non-root user
|
# Install OpenSSL for Prisma and su-exec, create non-root user
|
||||||
RUN apk add --no-cache openssl su-exec sqlite-libs && \
|
RUN apk add --no-cache openssl su-exec && \
|
||||||
addgroup -g 1001 -S nodejs && \
|
addgroup -g 1001 -S nodejs && \
|
||||||
adduser -S nodejs -u 1001
|
adduser -S nodejs -u 1001
|
||||||
|
|
||||||
|
|||||||
Generated
+1900
-343
File diff suppressed because it is too large
Load Diff
+10
-5
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.1.6",
|
"version": "0.2.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon src/index.ts",
|
"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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
"@types/archiver": "^7.0.0",
|
"@types/archiver": "^7.0.0",
|
||||||
"@types/jsdom": "^27.0.0",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/socket.io": "^3.0.1",
|
"@types/socket.io": "^3.0.1",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
"dompurify": "^3.3.0",
|
"dompurify": "^3.3.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^22.1.0",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"prisma": "^5.22.0",
|
"prisma": "^5.22.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
@@ -33,8 +35,11 @@
|
|||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/supertest": "^6.0.3",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
|
"supertest": "^7.1.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3",
|
||||||
|
"vitest": "^4.0.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,34 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "PrivateVault" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'vault',
|
|
||||||
"passwordHash" TEXT NOT NULL,
|
|
||||||
"salt" TEXT NOT NULL,
|
|
||||||
"hint" TEXT,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" DATETIME NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- RedefineTables
|
|
||||||
PRAGMA defer_foreign_keys=ON;
|
|
||||||
PRAGMA foreign_keys=OFF;
|
|
||||||
CREATE TABLE "new_Drawing" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"elements" TEXT NOT NULL,
|
|
||||||
"appState" TEXT NOT NULL,
|
|
||||||
"files" TEXT NOT NULL DEFAULT '{}',
|
|
||||||
"preview" TEXT,
|
|
||||||
"version" INTEGER NOT NULL DEFAULT 1,
|
|
||||||
"collectionId" TEXT,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" DATETIME NOT NULL,
|
|
||||||
"isPrivate" BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
"encryptedData" TEXT,
|
|
||||||
"iv" TEXT,
|
|
||||||
CONSTRAINT "Drawing_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
INSERT INTO "new_Drawing" ("appState", "collectionId", "createdAt", "elements", "files", "id", "name", "preview", "updatedAt", "version") SELECT "appState", "collectionId", "createdAt", "elements", "files", "id", "name", "preview", "updatedAt", "version" FROM "Drawing";
|
|
||||||
DROP TABLE "Drawing";
|
|
||||||
ALTER TABLE "new_Drawing" RENAME TO "Drawing";
|
|
||||||
PRAGMA foreign_keys=ON;
|
|
||||||
PRAGMA defer_foreign_keys=OFF;
|
|
||||||
@@ -32,21 +32,6 @@ model Drawing {
|
|||||||
collection Collection? @relation(fields: [collectionId], references: [id])
|
collection Collection? @relation(fields: [collectionId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
// Privacy/Encryption fields
|
|
||||||
isPrivate Boolean @default(false)
|
|
||||||
encryptedData String? // Encrypted blob containing elements, appState, files when isPrivate=true
|
|
||||||
iv String? // Initialization vector for AES-GCM decryption
|
|
||||||
}
|
|
||||||
|
|
||||||
// Singleton model for storing vault password hash and settings
|
|
||||||
model PrivateVault {
|
|
||||||
id String @id @default("vault") // Singleton pattern
|
|
||||||
passwordHash String // bcrypt hash for password verification
|
|
||||||
salt String // Salt for client-side key derivation (hex encoded)
|
|
||||||
hint String? // Optional password hint
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Library {
|
model Library {
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
|
||||||
/* eslint-disable */
|
|
||||||
// biome-ignore-all lint: generated file
|
|
||||||
// @ts-nocheck
|
|
||||||
/*
|
|
||||||
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
|
||||||
*
|
|
||||||
* 🟢 You can import this file directly.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type * as runtime from "@prisma/client/runtime/client"
|
|
||||||
import * as $Enums from "./enums"
|
|
||||||
import type * as Prisma from "./internal/prismaNamespace"
|
|
||||||
|
|
||||||
|
|
||||||
export type StringFilter<$PrismaModel = never> = {
|
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
in?: string[]
|
|
||||||
notIn?: string[]
|
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DateTimeFilter<$PrismaModel = never> = {
|
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
in?: Date[] | string[]
|
|
||||||
notIn?: Date[] | string[]
|
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
in?: string[]
|
|
||||||
notIn?: string[]
|
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
in?: Date[] | string[]
|
|
||||||
notIn?: Date[] | string[]
|
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IntFilter<$PrismaModel = never> = {
|
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
in?: number[]
|
|
||||||
notIn?: number[]
|
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type StringNullableFilter<$PrismaModel = never> = {
|
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
|
||||||
in?: string[] | null
|
|
||||||
notIn?: string[] | null
|
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SortOrderInput = {
|
|
||||||
sort: Prisma.SortOrder
|
|
||||||
nulls?: Prisma.NullsOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
in?: number[]
|
|
||||||
notIn?: number[]
|
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
|
||||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
|
||||||
in?: string[] | null
|
|
||||||
notIn?: string[] | null
|
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedStringFilter<$PrismaModel = never> = {
|
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
in?: string[]
|
|
||||||
notIn?: string[]
|
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
in?: Date[] | string[]
|
|
||||||
notIn?: Date[] | string[]
|
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
in?: string[]
|
|
||||||
notIn?: string[]
|
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedIntFilter<$PrismaModel = never> = {
|
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
in?: number[]
|
|
||||||
notIn?: number[]
|
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
in?: Date[] | string[]
|
|
||||||
notIn?: Date[] | string[]
|
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
|
||||||
in?: string[] | null
|
|
||||||
notIn?: string[] | null
|
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
in?: number[]
|
|
||||||
notIn?: number[]
|
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
|
||||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
|
||||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
|
||||||
in?: number[]
|
|
||||||
notIn?: number[]
|
|
||||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
|
||||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
|
||||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
|
||||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
|
||||||
in?: string[] | null
|
|
||||||
notIn?: string[] | null
|
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
|
||||||
in?: number[] | null
|
|
||||||
notIn?: number[] | null
|
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./index"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports = { ...require('.') }
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./default"
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,15 +0,0 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
|
||||||
/* eslint-disable */
|
|
||||||
// biome-ignore-all lint: generated file
|
|
||||||
// @ts-nocheck
|
|
||||||
/*
|
|
||||||
* This file exports all enum related types from the schema.
|
|
||||||
*
|
|
||||||
* 🟢 You can import this file directly.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// This file is empty because there are no enums in the schema.
|
|
||||||
export {}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user