Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb42187ba8 |
@@ -7,9 +7,3 @@ dist
|
|||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
backend
|
|
||||||
frontend/node_modules
|
|
||||||
frontend/dist
|
|
||||||
frontend/coverage
|
|
||||||
frontend/test-results
|
|
||||||
frontend/playwright-report
|
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
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:6767" 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:6767 > /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
|
|
||||||
+1
-110
@@ -1,112 +1,3 @@
|
|||||||
# 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
|
||||||
.DS_Store?
|
backend/prisma/*.db
|
||||||
._*
|
|
||||||
.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:./dev.db"
|
DATABASE_URL="file:./prisma/dev.db"
|
||||||
PORT=8000
|
PORT=8000
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
# Fork Summary
|
|
||||||
|
|
||||||
This fork adds optional security features and UX improvements with **zero breaking changes** and **minimal migration overhead**. All security features are **disabled by default** via feature flags.
|
|
||||||
|
|
||||||
## Security Features Added
|
|
||||||
|
|
||||||
1. **Password Reset** - Token-based password reset flow (`/auth/password-reset-request`, `/auth/password-reset-confirm`)
|
|
||||||
2. **Refresh Token Rotation** - Prevents token reuse by rotating refresh tokens on each use
|
|
||||||
3. **Audit Logging** - Logs security events (logins, password changes, deletions) for compliance
|
|
||||||
|
|
||||||
## UX Improvements Added
|
|
||||||
|
|
||||||
1. **Profile Page** - View and edit personal information, change password (`/profile`)
|
|
||||||
2. **Select All Button** - Quick selection of all drawings in current view
|
|
||||||
3. **Sort Dropdown** - Improved sort controls with icons and separate direction toggle
|
|
||||||
4. **Auto-hide Header** - Editor header auto-hides to maximize drawing space (with toggle)
|
|
||||||
|
|
||||||
## Backward Compatibility
|
|
||||||
|
|
||||||
✅ All security features disabled by default
|
|
||||||
✅ No breaking changes to existing code
|
|
||||||
✅ Graceful degradation (missing tables don't cause errors)
|
|
||||||
✅ Optional database migration
|
|
||||||
|
|
||||||
## Enable Security Features
|
|
||||||
|
|
||||||
Set in `backend/.env`:
|
|
||||||
```bash
|
|
||||||
ENABLE_PASSWORD_RESET=true
|
|
||||||
ENABLE_REFRESH_TOKEN_ROTATION=true
|
|
||||||
ENABLE_AUDIT_LOGGING=true
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run migration:
|
|
||||||
```bash
|
|
||||||
cd backend && npx prisma migrate deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
**For base project:** Keep features disabled (default) - no migration needed, zero risk.
|
|
||||||
|
|
||||||
**For this fork:** Enable features via environment variables when ready.
|
|
||||||
|
|
||||||
## Database Changes
|
|
||||||
|
|
||||||
Migration adds 3 optional tables (only used when features enabled):
|
|
||||||
- `PasswordResetToken` - For password reset flow
|
|
||||||
- `RefreshToken` - For token rotation tracking
|
|
||||||
- `AuditLog` - For security event logging
|
|
||||||
|
|
||||||
## Code Changes
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- Feature flags in `backend/src/config.ts`
|
|
||||||
- Conditional logic in auth endpoints
|
|
||||||
- Graceful error handling for missing tables
|
|
||||||
- New endpoints: `/auth/profile` (PUT), `/auth/change-password` (POST)
|
|
||||||
- Audit logging utility (`backend/src/utils/audit.ts`)
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- Password reset pages (`/reset-password`, `/reset-password-confirm`)
|
|
||||||
- Profile page (`/profile`)
|
|
||||||
- Select All button in Dashboard
|
|
||||||
- Sort dropdown with icons
|
|
||||||
- Auto-hide header in Editor with toggle
|
|
||||||
- Updated API client for token rotation
|
|
||||||
|
|
||||||
All changes are backward compatible and optional.
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
|
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
|
||||||
|
|
||||||
# ExcaliDash
|
# ExcaliDash v0.1.0
|
||||||
|
|
||||||

|
[](LICENSE)
|
||||||

|
|
||||||
[](https://hub.docker.com)
|
[](https://hub.docker.com)
|
||||||
|
|
||||||
A self-hosted dashboard and organizer for [Excalidraw](https://github.com/excalidraw/excalidraw) with live collaboration features.
|
A self-hosted dashboard and organizer for [Excalidraw](https://github.com/excalidraw/excalidraw) with live collaboration features.
|
||||||
@@ -22,8 +21,6 @@ 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)
|
||||||
@@ -77,10 +74,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.
|
> 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)
|
||||||
|
|
||||||
> [!CAUTION]
|
|
||||||
> ExcaliDash is in BETA. Please backup your data regularly (e.g. with cron).
|
|
||||||
|
|
||||||
## Docker Hub (Recommended)
|
## Docker Hub (Recommended)
|
||||||
|
|
||||||
@@ -99,8 +93,6 @@ docker compose -f docker-compose.prod.yml up -d
|
|||||||
# Access the frontend at localhost:6767
|
# Access the frontend at localhost:6767
|
||||||
```
|
```
|
||||||
|
|
||||||
For single-container deployments, `JWT_SECRET` can be omitted and will be auto-generated and persisted in the backend volume on first start. For portability and all multi-instance deployments, set a fixed `JWT_SECRET` explicitly.
|
|
||||||
|
|
||||||
## Docker Build
|
## Docker Build
|
||||||
|
|
||||||
[Install Docker](https://docs.docker.com/desktop/)
|
[Install Docker](https://docs.docker.com/desktop/)
|
||||||
@@ -118,48 +110,6 @@ 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. **Supports multiple comma-separated URLs** for accessing from different addresses.
|
|
||||||
- `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:
|
|
||||||
# Single URL
|
|
||||||
- FRONTEND_URL=https://excalidash.example.com
|
|
||||||
# Or multiple URLs (comma-separated) for local + network access
|
|
||||||
# - FRONTEND_URL=http://localhost:6767,http://192.168.1.100:6767,http://nas.local:6767
|
|
||||||
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 both `JWT_SECRET` and `CSRF_SECRET` to the same values across all instances.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate a secure secret
|
|
||||||
openssl rand -base64 32
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml or k8s deployment
|
|
||||||
backend:
|
|
||||||
environment:
|
|
||||||
- JWT_SECRET=your-generated-jwt-secret-here
|
|
||||||
- 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
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
Multi user setup is opt-in, single user by default
|
|
||||||
|
|
||||||
Multi-user support for excalidash
|
|
||||||
- Admin dashboard
|
|
||||||
- Password reset, force user password reset (admin only), account lockout recovery
|
|
||||||
- Rate limits
|
|
||||||
|
|
||||||
Deprecates .json and .sqlite database backups in favor of .excalidash archives (user scoped, prevents exporting of senstive information). Legacy import is maintained.
|
|
||||||
|
|
||||||
@@ -9,7 +9,3 @@ dist
|
|||||||
*.log
|
*.log
|
||||||
prisma/dev.db
|
prisma/dev.db
|
||||||
prisma/dev.db-journal
|
prisma/dev.db-journal
|
||||||
src/generated
|
|
||||||
coverage
|
|
||||||
*.test.ts
|
|
||||||
*.spec.ts
|
|
||||||
|
|||||||
@@ -2,11 +2,3 @@
|
|||||||
PORT=8000
|
PORT=8000
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
DATABASE_URL=file:/app/prisma/dev.db
|
DATABASE_URL=file:/app/prisma/dev.db
|
||||||
FRONTEND_URL=http://localhost:6767
|
|
||||||
JWT_SECRET=change-this-secret-in-production-min-32-chars
|
|
||||||
|
|
||||||
# Optional Feature Flags (all default to false for backward compatibility)
|
|
||||||
# Set to "true" or "1" to enable:
|
|
||||||
# ENABLE_PASSWORD_RESET=false
|
|
||||||
# ENABLE_REFRESH_TOKEN_ROTATION=false
|
|
||||||
# ENABLE_AUDIT_LOGGING=false
|
|
||||||
|
|||||||
+7
-17
@@ -3,15 +3,12 @@ FROM node:20-alpine AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Native build deps for modules that may compile from source (e.g., better-sqlite3 on arm64)
|
|
||||||
RUN apk add --no-cache python3 make g++
|
|
||||||
|
|
||||||
# Copy package files
|
# Copy package files
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
COPY tsconfig.json ./
|
COPY tsconfig.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci && npm cache clean --force
|
RUN npm ci
|
||||||
|
|
||||||
# Copy prisma schema
|
# Copy prisma schema
|
||||||
COPY prisma ./prisma/
|
COPY prisma ./prisma/
|
||||||
@@ -28,10 +25,8 @@ RUN npx tsc
|
|||||||
# Production stage
|
# Production stage
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
# Install runtime packages and create non-root user
|
# Install OpenSSL for Prisma
|
||||||
RUN apk add --no-cache openssl su-exec && \
|
RUN apk add --no-cache openssl
|
||||||
addgroup -g 1001 -S nodejs && \
|
|
||||||
adduser -S nodejs -u 1001
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -39,10 +34,7 @@ WORKDIR /app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install production dependencies only
|
# Install production dependencies only
|
||||||
RUN apk add --no-cache --virtual .build-deps python3 make g++ && \
|
RUN npm ci --only=production
|
||||||
npm ci --omit=dev && \
|
|
||||||
npm cache clean --force && \
|
|
||||||
apk del .build-deps
|
|
||||||
|
|
||||||
# Copy prisma schema and migrations for runtime and hydration template
|
# Copy prisma schema and migrations for runtime and hydration template
|
||||||
COPY prisma ./prisma/
|
COPY prisma ./prisma/
|
||||||
@@ -54,15 +46,13 @@ COPY --from=builder /app/dist ./dist
|
|||||||
# Copy the generated Prisma Client from builder to maintain the same structure
|
# Copy the generated Prisma Client from builder to maintain the same structure
|
||||||
COPY --from=builder /app/src/generated ./dist/generated
|
COPY --from=builder /app/src/generated ./dist/generated
|
||||||
|
|
||||||
# Create necessary directories (ownership will be set in entrypoint)
|
# Generate Prisma Client in production (updates node_modules)
|
||||||
RUN mkdir -p /app/uploads /app/prisma
|
RUN npx prisma generate
|
||||||
|
|
||||||
# Copy and set permissions for entrypoint script
|
# Run migrations and start server
|
||||||
COPY docker-entrypoint.sh ./
|
COPY docker-entrypoint.sh ./
|
||||||
RUN chmod +x docker-entrypoint.sh
|
RUN chmod +x docker-entrypoint.sh
|
||||||
|
|
||||||
# REMOVED: USER nodejs (We must stay root to fix permissions in entrypoint)
|
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,59 +1,14 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
JWT_SECRET_FILE="/app/prisma/.jwt_secret"
|
# Auto-hydrate prisma directory when bind-mounted volume is empty
|
||||||
|
|
||||||
# Ensure JWT secret exists for production startup.
|
|
||||||
# Backward compatibility: older installs may not have JWT_SECRET configured.
|
|
||||||
if [ -z "${JWT_SECRET:-}" ]; then
|
|
||||||
echo "JWT_SECRET not provided, resolving persisted secret..."
|
|
||||||
if [ -f "${JWT_SECRET_FILE}" ]; then
|
|
||||||
JWT_SECRET="$(tr -d '\r\n' < "${JWT_SECRET_FILE}")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${JWT_SECRET}" ]; then
|
|
||||||
echo "No persisted JWT secret found. Generating a new secret..."
|
|
||||||
JWT_SECRET="$(openssl rand -hex 32)"
|
|
||||||
umask 077
|
|
||||||
printf "%s" "${JWT_SECRET}" > "${JWT_SECRET_FILE}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Persist explicitly provided secret to support future restarts without env injection.
|
|
||||||
umask 077
|
|
||||||
printf "%s" "${JWT_SECRET}" > "${JWT_SECRET_FILE}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
export JWT_SECRET
|
|
||||||
|
|
||||||
# 1. Hydrate volume if empty (Running as root)
|
|
||||||
if [ ! -f "/app/prisma/schema.prisma" ]; then
|
if [ ! -f "/app/prisma/schema.prisma" ]; then
|
||||||
echo "Mount is empty. Hydrating /app/prisma..."
|
echo "Mount is empty. Hydrating /app/prisma from /app/prisma_template..."
|
||||||
cp -R /app/prisma_template/. /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
|
fi
|
||||||
|
|
||||||
# 2. Fix permissions unconditionally (Running as root)
|
# Run migrations
|
||||||
echo "Fixing filesystem permissions..."
|
npx prisma migrate deploy
|
||||||
chown -R nodejs:nodejs /app/uploads
|
|
||||||
chown -R nodejs:nodejs /app/prisma
|
|
||||||
chmod 755 /app/uploads
|
|
||||||
chmod 600 "${JWT_SECRET_FILE}"
|
|
||||||
|
|
||||||
# Ensure database file has proper permissions
|
# Start the application
|
||||||
if [ -f "/app/prisma/dev.db" ]; then
|
node dist/index.js
|
||||||
echo "Database file found, ensuring write permissions..."
|
|
||||||
chmod 666 /app/prisma/dev.db
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Run Migrations (Drop privileges to nodejs)
|
|
||||||
echo "Running database migrations..."
|
|
||||||
su-exec nodejs npx prisma migrate deploy
|
|
||||||
|
|
||||||
# 4. Start Application (Drop privileges to nodejs)
|
|
||||||
echo "Starting application as nodejs..."
|
|
||||||
exec su-exec nodejs node dist/index.js
|
|
||||||
|
|||||||
Generated
+40
-2489
File diff suppressed because it is too large
Load Diff
+7
-28
@@ -1,15 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.4.6",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "node scripts/predev-migrate.cjs",
|
|
||||||
"dev": "nodemon src/index.ts",
|
"dev": "nodemon src/index.ts",
|
||||||
"admin:recover": "node scripts/admin-recover.cjs",
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
"test": "vitest run",
|
|
||||||
"test:watch": "vitest",
|
|
||||||
"test:coverage": "vitest run --coverage"
|
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -17,42 +13,25 @@
|
|||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
"@types/archiver": "^7.0.0",
|
||||||
|
"@types/multer": "^2.0.0",
|
||||||
|
"@types/socket.io": "^3.0.1",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"bcrypt": "^6.0.0",
|
|
||||||
"better-sqlite3": "^12.4.6",
|
"better-sqlite3": "^12.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dompurify": "^3.3.0",
|
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^8.2.1",
|
|
||||||
"helmet": "^8.1.0",
|
|
||||||
"jsdom": "^22.1.0",
|
|
||||||
"jsonwebtoken": "^9.0.3",
|
|
||||||
"jszip": "^3.10.1",
|
|
||||||
"ms": "^2.1.3",
|
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"prisma": "^5.22.0",
|
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"uuid": "^13.0.0",
|
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/archiver": "^7.0.0",
|
|
||||||
"@types/bcrypt": "^6.0.0",
|
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
"@types/jsdom": "^21.1.7",
|
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
|
||||||
"@types/ms": "^2.1.0",
|
|
||||||
"@types/multer": "^2.0.0",
|
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/socket.io": "^3.0.1",
|
|
||||||
"@types/supertest": "^6.0.3",
|
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"supertest": "^7.1.4",
|
"prisma": "^5.22.0",
|
||||||
"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.
@@ -1,7 +0,0 @@
|
|||||||
-- 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
|
|
||||||
);
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
-- NOTE:
|
|
||||||
-- This migration assigns all pre-existing data to a bootstrap admin user so that
|
|
||||||
-- upgrading an existing (non-empty) database doesn't fail and the data remains accessible.
|
|
||||||
-- The bootstrap admin user starts inactive and must be activated via the app's
|
|
||||||
-- initial registration flow.
|
|
||||||
|
|
||||||
-- Constants
|
|
||||||
-- Keep in sync with backend/src/auth.ts
|
|
||||||
-- (SQLite doesn't support variables; we inline the values instead.)
|
|
||||||
-- BOOTSTRAP_USER_ID = 'bootstrap-admin'
|
|
||||||
-- BOOTSTRAP_LIBRARY_ID = 'user_bootstrap-admin'
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "User" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"username" TEXT,
|
|
||||||
"email" TEXT NOT NULL,
|
|
||||||
"passwordHash" TEXT NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"role" TEXT NOT NULL DEFAULT 'USER',
|
|
||||||
"mustResetPassword" BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" DATETIME NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "SystemConfig" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'default',
|
|
||||||
"registrationEnabled" BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" DATETIME NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Bootstrap state:
|
|
||||||
-- - Insert a singleton config row (registration disabled by default)
|
|
||||||
-- - Insert an inactive bootstrap admin user and assign all existing data to it
|
|
||||||
INSERT INTO "SystemConfig" ("id", "registrationEnabled", "createdAt", "updatedAt")
|
|
||||||
VALUES ('default', false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
|
|
||||||
|
|
||||||
INSERT INTO "User" ("id", "username", "email", "passwordHash", "name", "role", "mustResetPassword", "isActive", "createdAt", "updatedAt")
|
|
||||||
VALUES ('bootstrap-admin', NULL, 'bootstrap@excalidash.local', '', 'Bootstrap Admin', 'ADMIN', true, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
|
|
||||||
|
|
||||||
-- RedefineTables
|
|
||||||
PRAGMA defer_foreign_keys=ON;
|
|
||||||
PRAGMA foreign_keys=OFF;
|
|
||||||
CREATE TABLE "new_Collection" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"userId" TEXT NOT NULL,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" DATETIME NOT NULL,
|
|
||||||
CONSTRAINT "Collection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
INSERT INTO "new_Collection" ("createdAt", "id", "name", "userId", "updatedAt")
|
|
||||||
SELECT "createdAt", "id", "name", 'bootstrap-admin', "updatedAt" FROM "Collection";
|
|
||||||
DROP TABLE "Collection";
|
|
||||||
ALTER TABLE "new_Collection" RENAME TO "Collection";
|
|
||||||
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,
|
|
||||||
"userId" TEXT NOT NULL,
|
|
||||||
"collectionId" TEXT,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" DATETIME NOT NULL,
|
|
||||||
CONSTRAINT "Drawing_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
|
||||||
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", "userId", "updatedAt", "version")
|
|
||||||
SELECT "appState", "collectionId", "createdAt", "elements", "files", "id", "name", "preview", 'bootstrap-admin', "updatedAt", "version" FROM "Drawing";
|
|
||||||
DROP TABLE "Drawing";
|
|
||||||
ALTER TABLE "new_Drawing" RENAME TO "Drawing";
|
|
||||||
CREATE TABLE "new_Library" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"items" TEXT NOT NULL DEFAULT '[]',
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" DATETIME NOT NULL
|
|
||||||
);
|
|
||||||
-- Migrate the singleton library to the bootstrap user's library key.
|
|
||||||
INSERT INTO "new_Library" ("createdAt", "id", "items", "updatedAt")
|
|
||||||
SELECT "createdAt", 'user_bootstrap-admin', "items", "updatedAt" FROM "Library" WHERE "id" = 'default';
|
|
||||||
DROP TABLE "Library";
|
|
||||||
ALTER TABLE "new_Library" RENAME TO "Library";
|
|
||||||
PRAGMA foreign_keys=ON;
|
|
||||||
PRAGMA defer_foreign_keys=OFF;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "PasswordResetToken" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"userId" TEXT NOT NULL,
|
|
||||||
"token" TEXT NOT NULL,
|
|
||||||
"expiresAt" DATETIME NOT NULL,
|
|
||||||
"used" BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "RefreshToken" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"userId" TEXT NOT NULL,
|
|
||||||
"token" TEXT NOT NULL,
|
|
||||||
"expiresAt" DATETIME NOT NULL,
|
|
||||||
"revoked" BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "AuditLog" (
|
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"userId" TEXT,
|
|
||||||
"action" TEXT NOT NULL,
|
|
||||||
"resource" TEXT,
|
|
||||||
"ipAddress" TEXT,
|
|
||||||
"userAgent" TEXT,
|
|
||||||
"details" TEXT,
|
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "RefreshToken_token_key" ON "RefreshToken"("token");
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
-- Add authEnabled flag to SystemConfig to support single-user mode by default.
|
|
||||||
|
|
||||||
-- SQLite supports simple ADD COLUMN for non-null with default.
|
|
||||||
ALTER TABLE "SystemConfig" ADD COLUMN "authEnabled" BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
|
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "SystemConfig" ADD COLUMN "authLoginRateLimitEnabled" BOOLEAN NOT NULL DEFAULT 1;
|
|
||||||
ALTER TABLE "SystemConfig" ADD COLUMN "authLoginRateLimitWindowMs" INTEGER NOT NULL DEFAULT 900000;
|
|
||||||
ALTER TABLE "SystemConfig" ADD COLUMN "authLoginRateLimitMax" INTEGER NOT NULL DEFAULT 20;
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
-- Improve dashboard query performance for user-scoped collection and drawing listings.
|
|
||||||
CREATE INDEX IF NOT EXISTS "Collection_userId_updatedAt_idx"
|
|
||||||
ON "Collection" ("userId", "updatedAt");
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS "Drawing_userId_updatedAt_idx"
|
|
||||||
ON "Drawing" ("userId", "updatedAt");
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS "Drawing_userId_collectionId_updatedAt_idx"
|
|
||||||
ON "Drawing" ("userId", "collectionId", "updatedAt");
|
|
||||||
Binary file not shown.
@@ -12,45 +12,12 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
username String? @unique
|
|
||||||
email String @unique
|
|
||||||
passwordHash String
|
|
||||||
name String
|
|
||||||
role String @default("USER")
|
|
||||||
mustResetPassword Boolean @default(false)
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
drawings Drawing[]
|
|
||||||
collections Collection[]
|
|
||||||
passwordResetTokens PasswordResetToken[]
|
|
||||||
refreshTokens RefreshToken[]
|
|
||||||
auditLogs AuditLog[]
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
model SystemConfig {
|
|
||||||
id String @id @default("default")
|
|
||||||
authEnabled Boolean @default(false)
|
|
||||||
registrationEnabled Boolean @default(false)
|
|
||||||
authLoginRateLimitEnabled Boolean @default(true)
|
|
||||||
authLoginRateLimitWindowMs Int @default(900000) // 15 minutes
|
|
||||||
authLoginRateLimitMax Int @default(20)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
model Collection {
|
model Collection {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String
|
name String
|
||||||
userId String
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
drawings Drawing[]
|
drawings Drawing[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@index([userId, updatedAt])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Drawing {
|
model Drawing {
|
||||||
@@ -61,52 +28,8 @@ model Drawing {
|
|||||||
files String @default("{}") // Stored as JSON string
|
files String @default("{}") // Stored as JSON string
|
||||||
preview String? // SVG string for thumbnail
|
preview String? // SVG string for thumbnail
|
||||||
version Int @default(1)
|
version Int @default(1)
|
||||||
userId String
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
collectionId String?
|
collectionId String?
|
||||||
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
|
||||||
|
|
||||||
@@index([userId, updatedAt])
|
|
||||||
@@index([userId, collectionId, updatedAt])
|
|
||||||
}
|
|
||||||
|
|
||||||
model Library {
|
|
||||||
id String @id // User-specific library ID (e.g., "user_<userId>")
|
|
||||||
items String @default("[]") // Stored as JSON string array of library items
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
model PasswordResetToken {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
userId String
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
token String @unique
|
|
||||||
expiresAt DateTime
|
|
||||||
used Boolean @default(false)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
}
|
|
||||||
|
|
||||||
model RefreshToken {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
userId String
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
token String @unique
|
|
||||||
expiresAt DateTime
|
|
||||||
revoked Boolean @default(false)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
}
|
|
||||||
|
|
||||||
model AuditLog {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
userId String?
|
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
||||||
action String // e.g., "login", "login_failed", "password_reset", "password_changed", "drawing_deleted"
|
|
||||||
resource String? // e.g., "drawing:123", "collection:456"
|
|
||||||
ipAddress String?
|
|
||||||
userAgent String?
|
|
||||||
details String? // JSON string for additional details
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user