fix: pinning CORS to FRONTEND_URL, validate drawing payloads with Zod, staging SQLite imports with integrity checks and atomic swaps in index.ts

This commit is contained in:
Zimeng Xiong
2025-11-22 17:17:50 -08:00
parent b3dbcc2376
commit 5d5e22c8a1
4 changed files with 504 additions and 47 deletions
+8
View File
@@ -15,7 +15,9 @@ A self-hosted dashboard and organizer for [Excalidraw](https://github.com/excali
## Table of Contents ## Table of Contents
- [Screenshots](#screenshots)
- [Features](#features) - [Features](#features)
- [Upgrading](#upgrading)
- [Installation](#installation) - [Installation](#installation)
- [Docker Hub (Recommended)](#dockerhub-recommended) - [Docker Hub (Recommended)](#dockerhub-recommended)
- [Docker Build](#docker-build) - [Docker Build](#docker-build)
@@ -63,6 +65,12 @@ A self-hosted dashboard and organizer for [Excalidraw](https://github.com/excali
</details> </details>
# Upgrading
See [release notes](https://github.com/ZimengXiong/ExcaliDash/releases) for a specific release.
</details>
# Installation # Installation
> [!CAUTION] > [!CAUTION]
+332 -2
View File
@@ -14,11 +14,13 @@
"@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",
"better-sqlite3": "^12.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.1.0", "express": "^5.1.0",
"multer": "^2.0.2", "multer": "^2.0.2",
"socket.io": "^4.8.1" "socket.io": "^4.8.1",
"zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
@@ -590,6 +592,20 @@
"node": "^4.5.0 || >= 5.9" "node": "^4.5.0 || >= 5.9"
} }
}, },
"node_modules/better-sqlite3": {
"version": "12.4.6",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.6.tgz",
"integrity": "sha512-gaYt9yqTbQ1iOxLpJA8FPR5PiaHP+jlg8I5EX0Rs2KFwNzhBsF40KzMZS5FwelY7RG0wzaucWdqSAJM3uNCPCg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -603,6 +619,50 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@@ -760,6 +820,12 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -970,6 +1036,30 @@
} }
} }
}, },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -979,6 +1069,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/diff": { "node_modules/diff": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -1042,6 +1141,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/engine.io": { "node_modules/engine.io": {
"version": "6.6.4", "version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
@@ -1203,6 +1311,15 @@
"bare-events": "^2.7.0" "bare-events": "^2.7.0"
} }
}, },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/express": { "node_modules/express": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
@@ -1251,6 +1368,12 @@
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -1315,6 +1438,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1376,6 +1505,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/glob": { "node_modules/glob": {
"version": "10.5.0", "version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
@@ -1550,6 +1685,12 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -1775,6 +1916,18 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -1818,6 +1971,12 @@
"mkdirp": "bin/cmd.js" "mkdirp": "bin/cmd.js"
} }
}, },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1885,6 +2044,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@@ -1894,6 +2059,18 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node-abi": {
"version": "3.85.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.11", "version": "3.1.11",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
@@ -2037,6 +2214,32 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prisma": { "node_modules/prisma": {
"version": "5.22.0", "version": "5.22.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
@@ -2093,6 +2296,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@@ -2148,6 +2361,21 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": { "node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -2251,7 +2479,6 @@
"version": "7.7.3", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -2408,6 +2635,51 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-update-notifier": { "node_modules/simple-update-notifier": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@@ -2689,6 +2961,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -2702,6 +2983,34 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-fs/node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tar-stream": { "node_modules/tar-stream": {
"version": "3.1.7", "version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
@@ -2798,6 +3107,18 @@
} }
} }
}, },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/type-is": { "node_modules/type-is": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
@@ -3058,6 +3379,15 @@
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
},
"node_modules/zod": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }
+3 -1
View File
@@ -17,11 +17,13 @@
"@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",
"better-sqlite3": "^12.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.1.0", "express": "^5.1.0",
"multer": "^2.0.2", "multer": "^2.0.2",
"socket.io": "^4.8.1" "socket.io": "^4.8.1",
"zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
+161 -44
View File
@@ -7,6 +7,8 @@ import { createServer } from "http";
import { Server } from "socket.io"; import { Server } from "socket.io";
import multer from "multer"; import multer from "multer";
import archiver from "archiver"; import archiver from "archiver";
import Database from "better-sqlite3";
import { z } from "zod";
// @ts-ignore // @ts-ignore
import { PrismaClient } from "./generated/client"; import { PrismaClient } from "./generated/client";
@@ -36,11 +38,19 @@ const resolveDatabaseUrl = (rawUrl?: string) => {
process.env.DATABASE_URL = resolveDatabaseUrl(process.env.DATABASE_URL); process.env.DATABASE_URL = resolveDatabaseUrl(process.env.DATABASE_URL);
console.log("Resolved DATABASE_URL:", process.env.DATABASE_URL); console.log("Resolved DATABASE_URL:", process.env.DATABASE_URL);
const allowedOrigin = process.env.FRONTEND_URL || "http://localhost:6767";
const uploadDir = path.resolve(__dirname, "../uploads");
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const app = express(); const app = express();
const httpServer = createServer(app); const httpServer = createServer(app);
const io = new Server(httpServer, { const io = new Server(httpServer, {
cors: { cors: {
origin: "*", origin: allowedOrigin,
credentials: true,
}, },
maxHttpBufferSize: 1e8, // 100 MB maxHttpBufferSize: 1e8, // 100 MB
}); });
@@ -48,12 +58,82 @@ const prisma = new PrismaClient();
const PORT = process.env.PORT || 8000; const PORT = process.env.PORT || 8000;
// Multer setup for file uploads // Multer setup for file uploads
const upload = multer({ dest: "uploads/" }); const upload = multer({ dest: uploadDir });
app.use(cors()); app.use(
cors({
origin: allowedOrigin,
credentials: true,
})
);
app.use(express.json({ limit: "50mb" })); app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ extended: true, limit: "50mb" })); app.use(express.urlencoded({ extended: true, limit: "50mb" }));
const elementsSchema = z.array(z.object({}).passthrough());
const appStateSchema = z.object({}).passthrough();
const filesFieldSchema = z
.union([z.record(z.string(), z.any()), z.null()])
.optional()
.transform((value) => (value === null ? undefined : value));
const drawingBaseSchema = z.object({
name: z.string().trim().min(1).max(255).optional(),
collectionId: z.union([z.string().trim().min(1), z.null()]).optional(),
preview: z.string().nullable().optional(),
});
const drawingCreateSchema = drawingBaseSchema.extend({
elements: elementsSchema.default([]),
appState: appStateSchema.default({}),
files: filesFieldSchema,
});
const drawingUpdateSchema = drawingBaseSchema.extend({
elements: elementsSchema.optional(),
appState: appStateSchema.optional(),
files: filesFieldSchema,
});
const respondWithValidationErrors = (
res: express.Response,
issues: z.ZodIssue[]
) => {
res.status(400).json({
error: "Invalid drawing payload",
details: issues,
});
};
const runIntegrityCheck = (filePath: string): boolean => {
let dbInstance: Database.Database | undefined;
try {
dbInstance = new Database(filePath, {
readonly: true,
fileMustExist: true,
});
const result = dbInstance.prepare("PRAGMA integrity_check;").get();
return result?.integrity_check === "ok";
} catch (error) {
console.error("Integrity check failed:", error);
return false;
} finally {
dbInstance?.close();
}
};
const removeFileIfExists = (filePath?: string) => {
if (!filePath) return;
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (error) {
console.error("Failed to remove file", { filePath, error });
}
};
// Socket.io Logic // Socket.io Logic
interface User { interface User {
id: string; id: string;
@@ -213,16 +293,24 @@ app.get("/drawings/:id", async (req, res) => {
// POST /drawings // POST /drawings
app.post("/drawings", async (req, res) => { app.post("/drawings", async (req, res) => {
try { try {
const { name, elements, appState, collectionId, preview, files } = req.body; const parsed = drawingCreateSchema.safeParse(req.body);
if (!parsed.success) {
return respondWithValidationErrors(res, parsed.error.issues);
}
const payload = parsed.data;
const drawingName = payload.name ?? "Untitled Drawing";
const targetCollectionId =
payload.collectionId === undefined ? null : payload.collectionId;
const newDrawing = await prisma.drawing.create({ const newDrawing = await prisma.drawing.create({
data: { data: {
name, name: drawingName,
elements: JSON.stringify(elements || []), elements: JSON.stringify(payload.elements),
appState: JSON.stringify(appState || {}), appState: JSON.stringify(payload.appState),
collectionId: collectionId || null, collectionId: targetCollectionId,
preview: preview || null, preview: payload.preview ?? null,
files: JSON.stringify(files || {}), files: JSON.stringify(payload.files ?? {}),
}, },
}); });
@@ -241,28 +329,37 @@ app.post("/drawings", async (req, res) => {
app.put("/drawings/:id", async (req, res) => { app.put("/drawings/:id", async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { name, elements, appState, collectionId, preview, files } = req.body; const parsed = drawingUpdateSchema.safeParse(req.body);
if (!parsed.success) {
return respondWithValidationErrors(res, parsed.error.issues);
}
const payload = parsed.data;
console.log("[API] Updating drawing", { console.log("[API] Updating drawing", {
id, id,
hasElements: elements !== undefined, hasElements: payload.elements !== undefined,
elementCount: elementCount: Array.isArray(payload.elements)
elements && Array.isArray(elements) ? elements.length : undefined, ? payload.elements.length
hasAppState: appState !== undefined, : undefined,
hasFiles: files !== undefined, hasAppState: payload.appState !== undefined,
hasPreview: preview !== undefined, hasFiles: payload.files !== undefined,
hasPreview: payload.preview !== undefined,
}); });
const data: any = { const data: any = {
version: { increment: 1 }, version: { increment: 1 },
}; };
if (name !== undefined) data.name = name; if (payload.name !== undefined) data.name = payload.name;
if (elements !== undefined) data.elements = JSON.stringify(elements); if (payload.elements !== undefined)
if (appState !== undefined) data.appState = JSON.stringify(appState); data.elements = JSON.stringify(payload.elements);
if (files !== undefined) data.files = JSON.stringify(files); if (payload.appState !== undefined)
if (collectionId !== undefined) data.collectionId = collectionId; data.appState = JSON.stringify(payload.appState);
if (preview !== undefined) data.preview = preview; if (payload.files !== undefined) data.files = JSON.stringify(payload.files);
if (payload.collectionId !== undefined)
data.collectionId = payload.collectionId;
if (payload.preview !== undefined) data.preview = payload.preview;
const updatedDrawing = await prisma.drawing.update({ const updatedDrawing = await prisma.drawing.update({
where: { id }, where: { id },
@@ -528,24 +625,19 @@ app.post("/import/sqlite/verify", upload.single("db"), async (req, res) => {
return res.status(400).json({ error: "No file uploaded" }); return res.status(400).json({ error: "No file uploaded" });
} }
// Basic verification: check if it's a SQLite file const stagedPath = req.file.path;
const buffer = fs.readFileSync(req.file.path); const isValid = runIntegrityCheck(stagedPath);
const header = buffer.slice(0, 16).toString("ascii"); removeFileIfExists(stagedPath);
if (!header.startsWith("SQLite format 3")) { if (!isValid) {
fs.unlinkSync(req.file.path);
return res.status(400).json({ error: "Invalid SQLite file" }); return res.status(400).json({ error: "Invalid SQLite file" });
} }
// Additional verification could be added here
// For now, we'll just check the file signature
fs.unlinkSync(req.file.path);
res.json({ valid: true, message: "Database file is valid" }); res.json({ valid: true, message: "Database file is valid" });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (req.file && fs.existsSync(req.file.path)) { if (req.file) {
fs.unlinkSync(req.file.path); removeFileIfExists(req.file.path);
} }
res.status(500).json({ error: "Failed to verify database file" }); res.status(500).json({ error: "Failed to verify database file" });
} }
@@ -558,17 +650,42 @@ app.post("/import/sqlite", upload.single("db"), async (req, res) => {
return res.status(400).json({ error: "No file uploaded" }); return res.status(400).json({ error: "No file uploaded" });
} }
const dbPath = path.resolve(__dirname, "../prisma/dev.db"); const originalPath = req.file.path;
const stagedPath = path.join(
uploadDir,
`temp-${Date.now()}-${Math.random().toString(36).slice(2)}.db`
);
// Backup current database try {
if (fs.existsSync(dbPath)) { fs.renameSync(originalPath, stagedPath);
const backupPath = path.resolve(__dirname, "../prisma/dev.db.backup"); } catch (error) {
fs.copyFileSync(dbPath, backupPath); console.error("Failed to stage uploaded database", error);
removeFileIfExists(originalPath);
removeFileIfExists(stagedPath);
return res.status(500).json({ error: "Failed to stage uploaded file" });
} }
// Replace database file const isValid = runIntegrityCheck(stagedPath);
fs.copyFileSync(req.file.path, dbPath); if (!isValid) {
fs.unlinkSync(req.file.path); removeFileIfExists(stagedPath);
return res
.status(400)
.json({ error: "Uploaded database failed integrity check" });
}
const dbPath = path.resolve(__dirname, "../prisma/dev.db");
const backupPath = path.resolve(__dirname, "../prisma/dev.db.backup");
try {
if (fs.existsSync(dbPath)) {
fs.copyFileSync(dbPath, backupPath);
}
fs.renameSync(stagedPath, dbPath);
} catch (error) {
console.error("Failed to replace database", error);
removeFileIfExists(stagedPath);
return res.status(500).json({ error: "Failed to replace database" });
}
// Reinitialize Prisma client // Reinitialize Prisma client
await prisma.$disconnect(); await prisma.$disconnect();
@@ -576,8 +693,8 @@ app.post("/import/sqlite", upload.single("db"), async (req, res) => {
res.json({ success: true, message: "Database imported successfully" }); res.json({ success: true, message: "Database imported successfully" });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (req.file && fs.existsSync(req.file.path)) { if (req.file) {
fs.unlinkSync(req.file.path); removeFileIfExists(req.file.path);
} }
res.status(500).json({ error: "Failed to import database" }); res.status(500).json({ error: "Failed to import database" });
} }