From 633875e8ec90aa512dac8e2e54ea825d365db202 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 7 Jan 2026 14:57:36 +0100 Subject: [PATCH] feat(security): implement security fixes - Add AES-256-GCM encryption for Facebook/Instagram tokens - Add helmet for HTTP security headers - Add rate limiting (5 login attempts, 100 req/15min) - Add file upload validation (50MB max, MIME type whitelist) - Secure SQL console endpoint (disabled in production) - Enforce SESSION_SECRET in production --- .env.example | 4 + package-lock.json | 867 ++++++++++++++++++++++++------------- package.json | 4 +- server/index.ts | 75 +++- server/routes.ts | 280 ++++++------ server/storage.ts | 45 +- server/utils/encryption.ts | 92 ++++ 7 files changed, 917 insertions(+), 450 deletions(-) create mode 100644 server/utils/encryption.ts diff --git a/.env.example b/.env.example index 7110a84..2f44c2f 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,10 @@ APP_URL=http://localhost:5555 # Clé secrète pour les sessions (CHANGEZ CETTE VALEUR) SESSION_SECRET=your-secret-key-change-me-to-random-string +# Clé de chiffrement pour les tokens Facebook/Instagram (OBLIGATOIRE en production) +# Générer avec: openssl rand -hex 32 +ENCRYPTION_KEY=your-32-byte-hex-key-here + # Clé API OpenRouter pour la génération de texte IA # Obtenez votre clé sur https://openrouter.ai/ OPENROUTER_API_KEY=your-openrouter-api-key-here diff --git a/package-lock.json b/package-lock.json index 8a9d282..d8d515f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "@types/node-fetch": "^2.6.13", "@types/pg": "^8.15.5", "bcrypt": "^6.0.0", - "canvas": "^3.2.0", + "canvas": "^2.11.2", "class-variance-authority": "^0.7.1", "cloudinary": "^2.7.0", "clsx": "^2.1.1", @@ -62,8 +62,10 @@ "embla-carousel-react": "^8.6.0", "emoji-regex": "^10.6.0", "express": "^4.21.2", + "express-rate-limit": "^7.5.1", "express-session": "^1.18.1", "framer-motion": "^11.13.1", + "helmet": "^8.1.0", "html-to-image": "^1.11.13", "html2canvas": "^1.4.1", "input-otp": "^1.4.2", @@ -1909,6 +1911,38 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@neondatabase/serverless": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.10.4.tgz", @@ -4272,6 +4306,12 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -4285,6 +4325,18 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -4334,6 +4386,26 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4426,26 +4498,6 @@ "node": ">= 0.6.0" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "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/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -4472,17 +4524,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -4576,30 +4617,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "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/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4703,25 +4720,20 @@ "license": "CC-BY-4.0" }, "node_modules/canvas": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.0.tgz", - "integrity": "sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.3" + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" }, "engines": { - "node": "^18.12.0 || >= 20.9.0" + "node": ">=6" } }, - "node_modules/canvas/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4759,10 +4771,13 @@ } }, "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" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } }, "node_modules/class-variance-authority": { "version": "0.7.1", @@ -4829,6 +4844,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4850,6 +4874,12 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -4877,6 +4907,12 @@ "node": "^18.18.0 || ^20.9.0 || >=22.0.0" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -5115,27 +5151,15 @@ "license": "MIT" }, "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==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", "license": "MIT", "dependencies": { - "mimic-response": "^3.1.0" + "mimic-response": "^2.0.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": ">=8" } }, "node_modules/define-data-property": { @@ -5164,6 +5188,12 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5442,15 +5472,6 @@ "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/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -5595,15 +5616,6 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, - "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": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -5650,6 +5662,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express-session": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", @@ -5907,12 +5934,6 @@ "node": ">= 0.6" } }, - "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/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -5936,6 +5957,42 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5959,6 +6016,74 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -6027,12 +6152,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "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": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -6121,6 +6240,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6133,6 +6258,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/html-to-image": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", @@ -6168,6 +6302,19 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6180,25 +6327,16 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause" + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } }, "node_modules/inherits": { "version": "2.0.4", @@ -6206,12 +6344,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "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/input-otp": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", @@ -6690,6 +6822,21 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6811,12 +6958,12 @@ } }, "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6855,6 +7002,37 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -6873,12 +7051,6 @@ "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/modern-screenshot": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/modern-screenshot/-/modern-screenshot-4.6.0.tgz", @@ -6930,6 +7102,12 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", + "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -6947,12 +7125,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "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": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -6971,30 +7143,6 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/node-abi": { - "version": "3.78.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", - "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", @@ -7013,6 +7161,26 @@ "node": ">=6.0.0" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -7031,6 +7199,21 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7050,6 +7233,19 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7168,6 +7364,15 @@ "node": ">= 0.4.0" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7538,32 +7743,6 @@ "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", "license": "MIT" }, - "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/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7598,16 +7777,6 @@ "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", "license": "ISC" }, - "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/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -7687,21 +7856,6 @@ "node": ">= 0.8" } }, - "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/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -8018,6 +8172,65 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rollup": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.4.tgz", @@ -8118,7 +8331,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8187,6 +8399,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -8336,26 +8554,12 @@ "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" - } - ], + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", "license": "MIT", "dependencies": { - "decompress-response": "^6.0.0", + "decompress-response": "^4.2.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } @@ -8527,15 +8731,6 @@ "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/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -8632,34 +8827,50 @@ "node": ">=6" } }, - "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", + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "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" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -8717,6 +8928,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -8749,18 +8966,6 @@ "fsevents": "~2.3.3" } }, - "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/tw-animate-css": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz", @@ -9494,6 +9699,22 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9509,6 +9730,56 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wouter": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/wouter/-/wouter-3.3.5.tgz", diff --git a/package.json b/package.json index b3bdc7a..6f78300 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,10 @@ "embla-carousel-react": "^8.6.0", "emoji-regex": "^10.6.0", "express": "^4.21.2", + "express-rate-limit": "^7.5.1", "express-session": "^1.18.1", "framer-motion": "^11.13.1", + "helmet": "^8.1.0", "html-to-image": "^1.11.13", "html2canvas": "^1.4.1", "input-otp": "^1.4.2", @@ -125,4 +127,4 @@ "optionalDependencies": { "bufferutil": "^4.0.8" } -} \ No newline at end of file +} diff --git a/server/index.ts b/server/index.ts index bf61ba0..e756911 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2,6 +2,9 @@ import express, { type Request, Response, NextFunction } from "express"; import session from "express-session"; import connectPgSimple from "connect-pg-simple"; import pg from "pg"; +import crypto from "crypto"; +import helmet from "helmet"; +import rateLimit from "express-rate-limit"; import passport from "./auth"; import { registerRoutes } from "./routes"; import { setupVite, serveStatic, log } from "./vite"; @@ -11,6 +14,42 @@ import { ensureAdminUserExists } from "./init-admin"; const app = express(); const PgSession = connectPgSimple(session); +// Headers de sécurité HTTP avec helmet +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], + styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], + imgSrc: ["'self'", "data:", "https:", "blob:"], + fontSrc: ["'self'", "https://fonts.gstatic.com"], + connectSrc: ["'self'", "https://graph.facebook.com", "https://openrouter.ai", "https://res.cloudinary.com", "wss:", "ws:"], + } + }, + crossOriginEmbedderPolicy: false, +})); + +// Rate limiting global - 100 requêtes par 15 minutes par IP +const globalLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + message: { error: 'Trop de requêtes, réessayez plus tard' }, + standardHeaders: true, + legacyHeaders: false, +}); + +// Rate limiting strict pour l'authentification - 5 tentatives par 15 minutes +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5, + message: { error: 'Trop de tentatives de connexion, réessayez dans 15 minutes' }, + standardHeaders: true, + legacyHeaders: false, +}); + +app.use('/api/', globalLimiter); +app.use('/api/auth/login', authLimiter); + declare module 'http' { interface IncomingMessage { rawBody: unknown @@ -23,28 +62,36 @@ app.use(express.json({ })); app.use(express.urlencoded({ extended: false })); -// Configuration des sessions -if (!process.env.SESSION_SECRET) { - console.warn('⚠️ SESSION_SECRET non défini. Utilisation d\'une clé par défaut (NON SÉCURISÉ en production)'); +// Validation renforcée du SESSION_SECRET +if (!process.env.SESSION_SECRET && process.env.NODE_ENV === 'production') { + console.error('❌ SESSION_SECRET non défini en production. Arrêt du serveur.'); + process.exit(1); } +if (!process.env.SESSION_SECRET) { + console.warn('⚠️ SESSION_SECRET non défini. Utilisation d\'une clé aléatoire pour le développement.'); +} + +// Générer un secret aléatoire pour le dev si non défini +const sessionSecret = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex'); + // Déterminer si on utilise HTTPS basé sur APP_URL const isHttps = process.env.APP_URL?.startsWith('https://') || false; // Configuration du store de session pour production const sessionStore = process.env.NODE_ENV === 'production' && process.env.DATABASE_URL ? new PgSession({ - pool: new pg.Pool({ - connectionString: process.env.DATABASE_URL, - }), - tableName: 'session', - createTableIfMissing: true, - }) + pool: new pg.Pool({ + connectionString: process.env.DATABASE_URL, + }), + tableName: 'session', + createTableIfMissing: true, + }) : undefined; // MemoryStore par défaut en dev app.use(session({ store: sessionStore, - secret: process.env.SESSION_SECRET || 'your-secret-key-change-me', + secret: sessionSecret, resave: false, saveUninitialized: false, cookie: { @@ -77,7 +124,7 @@ app.use((req, res, next) => { if (path === "/api/auth/session" && res.statusCode === 401) { return; } - + let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`; if (capturedJsonResponse) { logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`; @@ -119,17 +166,17 @@ app.use((req, res, next) => { // this serves both the API and the client. // It is the only port that is not firewalled. const port = parseInt(process.env.PORT || '5000', 10); - + // Initialiser l'utilisateur admin par défaut avant de démarrer le serveur await ensureAdminUserExists(); - + server.listen({ port, host: "0.0.0.0", reusePort: true, }, () => { log(`serving on port ${port}`); - + // Démarrer le scheduler pour les publications programmées schedulerService.start(); }); diff --git a/server/routes.ts b/server/routes.ts index 0868df0..dbdb080 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -11,7 +11,27 @@ import { cloudinaryService } from "./services/cloudinary"; import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema"; import type { User, InsertUser, ScheduledPost } from "@shared/schema"; -const upload = multer({ storage: multer.memoryStorage() }); +// Types MIME autorisés pour les uploads +const ALLOWED_MIME_TYPES = [ + 'image/jpeg', 'image/png', 'image/gif', 'image/webp', + 'video/mp4', 'video/quicktime', 'video/webm' +]; + +// Configuration multer avec validation de taille et type +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 50 * 1024 * 1024, // 50MB max + files: 10 // 10 fichiers max + }, + fileFilter: (req, file, cb) => { + if (ALLOWED_MIME_TYPES.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error(`Type de fichier non autorisé: ${file.mimetype}`)); + } + } +}); // Middleware pour vérifier l'authentification function requireAuth(req: Request, res: Response, next: NextFunction) { @@ -83,14 +103,14 @@ export async function registerRoutes(app: Express): Promise { app.get("/api/users", requireAdmin, async (req, res) => { try { const allUsers = await storage.getAllUsers(); - + // Ne pas envoyer les mots de passe const safeUsers = allUsers.map(user => ({ id: user.id, username: user.username, role: user.role, })); - + res.json(safeUsers); } catch (error) { console.error("Error fetching users:", error); @@ -143,7 +163,7 @@ export async function registerRoutes(app: Express): Promise { try { const userId = req.params.id; const { username, password, role } = req.body; - + // Vérifier si l'utilisateur existe const existingUser = await storage.getUser(userId); if (!existingUser) { @@ -151,7 +171,7 @@ export async function registerRoutes(app: Express): Promise { } const updateData: Partial = {}; - + if (username && username !== existingUser.username) { // Vérifier si le nouveau username est déjà pris const userWithSameUsername = await storage.getUserByUsername(username); @@ -160,12 +180,12 @@ export async function registerRoutes(app: Express): Promise { } updateData.username = username; } - + if (password) { // Hasher le nouveau mot de passe updateData.password = await bcrypt.hash(password, 10); } - + if (role && (role === "admin" || role === "user")) { updateData.role = role; } @@ -175,7 +195,7 @@ export async function registerRoutes(app: Express): Promise { } const updatedUser = await storage.updateUser(userId, updateData); - + res.json({ id: updatedUser.id, username: updatedUser.username, @@ -192,7 +212,7 @@ export async function registerRoutes(app: Express): Promise { try { const userId = req.params.id; const currentUser = req.user as User; - + // Empêcher l'admin de se supprimer lui-même if (userId === currentUser.id) { return res.status(400).json({ error: "Vous ne pouvez pas supprimer votre propre compte" }); @@ -205,7 +225,7 @@ export async function registerRoutes(app: Express): Promise { } await storage.deleteUser(userId); - + res.json({ success: true, message: "Utilisateur supprimé avec succès" }); } catch (error: any) { console.error("Error deleting user:", error); @@ -240,7 +260,7 @@ export async function registerRoutes(app: Express): Promise { // Créer les nouvelles permissions const permissions = await Promise.all( - pageIds.map(pageId => + pageIds.map(pageId => storage.createPagePermission({ userId, pageId }) ) ); @@ -276,10 +296,10 @@ export async function registerRoutes(app: Express): Promise { } } - res.json({ - success: true, + res.json({ + success: true, message: `${migratedCount} permissions migrées avec succès`, - migratedCount + migratedCount }); } catch (error) { console.error("Error migrating permissions:", error); @@ -291,14 +311,14 @@ export async function registerRoutes(app: Express): Promise { app.get("/api/auth/default-password-status", async (req, res) => { try { const adminUser = await storage.getUserByUsername("admin"); - + if (!adminUser) { return res.json({ isDefault: false }); } // Vérifier si le mot de passe correspond à "admin" const isDefaultPassword = await bcrypt.compare("admin", adminUser.password); - + res.json({ isDefault: isDefaultPassword }); } catch (error) { console.error("Error checking default password:", error); @@ -306,8 +326,16 @@ export async function registerRoutes(app: Express): Promise { } }); - // Route SQL (réservée aux admins) + // Route SQL (réservée aux admins) - DÉSACTIVÉE EN PRODUCTION sauf si explicitement autorisée app.post("/api/sql/execute", requireAdmin, async (req, res) => { + // Vérification de sécurité: désactivé en production sauf si ALLOW_SQL_CONSOLE=true + if (process.env.NODE_ENV === 'production' && process.env.ALLOW_SQL_CONSOLE !== 'true') { + return res.status(403).json({ + success: false, + error: "Console SQL désactivée en production pour des raisons de sécurité" + }); + } + try { // Validation Zod const sqlQuerySchema = z.object({ @@ -318,7 +346,7 @@ export async function registerRoutes(app: Express): Promise { const { db } = await import("./db"); const result = await db.execute(validatedData.query); - + res.json({ success: true, result, @@ -345,7 +373,7 @@ export async function registerRoutes(app: Express): Promise { const result = await db.execute<{ tablename: string }>( `SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename;` ); - + res.json({ tables: result.rows || result, }); @@ -363,7 +391,7 @@ export async function registerRoutes(app: Express): Promise { try { const user = req.user as User; const userId = user.id; - + const posts = await storage.getPosts(userId); const pages = await storage.getSocialPages(userId); const media = await storage.getMedia(userId); @@ -379,7 +407,7 @@ export async function registerRoutes(app: Express): Promise { const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); yesterday.setHours(0, 0, 0, 0); - + const lastMonth = new Date(now); lastMonth.setMonth(lastMonth.getMonth() - 1); @@ -399,7 +427,7 @@ export async function registerRoutes(app: Express): Promise { }).length; // Calculer les variations en pourcentage - const aiTextChange = aiTextsYesterday > 0 + const aiTextChange = aiTextsYesterday > 0 ? Math.round(((currentAiTexts - aiTextsYesterday) / aiTextsYesterday) * 100) : currentAiTexts > 0 ? 100 : 0; @@ -469,10 +497,10 @@ export async function registerRoutes(app: Express): Promise { // Check if Cloudinary is configured (shared config used for all users) const cloudinaryConfig = await storage.getAnyCloudinaryConfig(); - + if (!cloudinaryConfig) { - return res.status(400).json({ - error: "Cloudinary not configured. Please ask an administrator to configure Cloudinary in Settings first." + return res.status(400).json({ + error: "Cloudinary not configured. Please ask an administrator to configure Cloudinary in Settings first." }); } @@ -543,7 +571,7 @@ export async function registerRoutes(app: Express): Promise { // Load with Sharp to process const sharp = (await import("sharp")).default; let image = sharp(imageBuffer); - + // Get image metadata for dimensions const metadata = await image.metadata(); const width = metadata.width!; @@ -562,33 +590,33 @@ export async function registerRoutes(app: Express): Promise { // Position mapping for each corner const positions: Record = { - north_west: { - x: 0, - y: 0, + north_west: { + x: 0, + y: 0, polygon: `0,0 ${ribbonSize},0 0,${ribbonSize}`, // Top-left triangle textX: ribbonSize * 0.3, textY: ribbonSize * 0.3, textRotation: -45 }, - north_east: { - x: width - ribbonSize, - y: 0, + north_east: { + x: width - ribbonSize, + y: 0, polygon: `0,0 ${ribbonSize},0 ${ribbonSize},${ribbonSize}`, // Top-right triangle textX: ribbonSize * 0.7, textY: ribbonSize * 0.3, textRotation: 45 }, - south_west: { - x: 0, - y: height - ribbonSize, + south_west: { + x: 0, + y: height - ribbonSize, polygon: `0,0 0,${ribbonSize} ${ribbonSize},${ribbonSize}`, // Bottom-left triangle textX: ribbonSize * 0.3, textY: ribbonSize * 0.7, textRotation: -135 }, - south_east: { - x: width - ribbonSize, - y: height - ribbonSize, + south_east: { + x: width - ribbonSize, + y: height - ribbonSize, polygon: `${ribbonSize},0 0,${ribbonSize} ${ribbonSize},${ribbonSize}`, // Bottom-right triangle textX: ribbonSize * 0.7, textY: ribbonSize * 0.7, @@ -642,8 +670,8 @@ export async function registerRoutes(app: Express): Promise { const badgeSvg = ` - + ${badgeText} @@ -661,7 +689,7 @@ export async function registerRoutes(app: Express): Promise { // Add logo overlay if (logo && logo.enabled) { console.log("🏢 Adding logo overlay..."); - + // Get Cloudinary config to get logo public ID const cloudinaryConfig = await storage.getAnyCloudinaryConfig(); if (cloudinaryConfig && cloudinaryConfig.logoPublicId) { @@ -669,44 +697,44 @@ export async function registerRoutes(app: Express): Promise { // Build logo URL from Cloudinary const logoUrl = `https://res.cloudinary.com/${cloudinaryConfig.cloudName}/image/upload/${cloudinaryConfig.logoPublicId}`; console.log("📥 Downloading logo from:", logoUrl); - + // Download logo const logoResponse = await fetch(logoUrl); const logoBuffer = Buffer.from(await logoResponse.arrayBuffer()); - + // Determine logo size based on selection with safety cap const padding = 20; const maxLogoWidth = width - (padding * 2); // Ensure logo fits within canvas - + const logoSizePercentages = { small: 0.35, // 35% of image width medium: 0.50, // 50% of image width large: 0.70 // 70% of image width }; - + const requestedWidth = Math.round(width * logoSizePercentages[logo.size as keyof typeof logoSizePercentages] || logoSizePercentages.medium); const logoWidth = Math.min(requestedWidth, maxLogoWidth); - + // Resize logo preserving aspect ratio and apply opacity const resizedLogo = await sharp(logoBuffer) .resize({ width: logoWidth, fit: 'contain' }) .ensureAlpha() .toBuffer(); - + // Get resized logo dimensions const logoMetadata = await sharp(resizedLogo).metadata(); const logoHeight = logoMetadata.height || logoWidth; - + // Ensure logo fits within height as well const maxLogoHeight = height - (padding * 2); if (logoHeight > maxLogoHeight) { console.warn(`⚠️ Logo height ${logoHeight}px exceeds max ${maxLogoHeight}px, would be clipped`); } - + // Calculate position with padding let logoX = padding; let logoY = padding; - + if (logo.position === 'north_east') { logoX = width - logoWidth - padding; } else if (logo.position === 'south_west') { @@ -718,7 +746,7 @@ export async function registerRoutes(app: Express): Promise { logoX = Math.round((width - logoWidth) / 2); logoY = Math.round((height - logoHeight) / 2); } - + // Apply opacity by manipulating alpha channel let logoWithOpacity = resizedLogo; if (logo.opacity < 100) { @@ -729,7 +757,7 @@ export async function registerRoutes(app: Express): Promise { .linear(opacityFactor, 0) .toBuffer(); } - + // Add logo overlay overlays.push({ input: logoWithOpacity, @@ -737,7 +765,7 @@ export async function registerRoutes(app: Express): Promise { left: logoX, blend: 'over' }); - + console.log(`✅ Logo added at position ${logo.position} with ${logo.opacity}% opacity`); } catch (logoError) { console.error("⚠️ Failed to apply logo:", logoError); @@ -761,8 +789,8 @@ export async function registerRoutes(app: Express): Promise { // Check if Cloudinary is configured (shared config used for all users) const cloudinaryConfig = await storage.getAnyCloudinaryConfig(); if (!cloudinaryConfig) { - return res.status(400).json({ - error: "Cloudinary not configured. Please ask an administrator to configure Cloudinary in Settings first." + return res.status(400).json({ + error: "Cloudinary not configured. Please ask an administrator to configure Cloudinary in Settings first." }); } @@ -804,11 +832,11 @@ export async function registerRoutes(app: Express): Promise { const user = req.user as User; const userId = user.id; const mediaId = req.params.id; - + // Get media to find cloudinary public ID const allMedia = await storage.getMedia(userId); const mediaToDelete = allMedia.find(m => m.id === mediaId); - + if (!mediaToDelete) { return res.status(404).json({ error: "Media not found" }); } @@ -816,7 +844,7 @@ export async function registerRoutes(app: Express): Promise { // Delete from Cloudinary if (mediaToDelete.cloudinaryPublicId) { await cloudinaryService.deleteMedia( - mediaToDelete.cloudinaryPublicId, + mediaToDelete.cloudinaryPublicId, userId, mediaToDelete.type ); @@ -824,7 +852,7 @@ export async function registerRoutes(app: Express): Promise { // Delete from database await storage.deleteMedia(mediaId); - + res.json({ success: true }); } catch (error) { console.error("Error deleting media:", error); @@ -850,10 +878,10 @@ export async function registerRoutes(app: Express): Promise { const user = req.user as User; const userId = user.id; const { pageIds, postType, mediaId, mediaIds, ...postFields } = req.body; - + // Convert mediaIds to standardized format: array of { mediaId, displayOrder } let finalMediaItems: Array<{ mediaId: string; displayOrder: number }> = []; - + if (mediaIds && Array.isArray(mediaIds)) { // New format: array of objects or strings finalMediaItems = mediaIds.map((item: any, index: number) => { @@ -873,52 +901,52 @@ export async function registerRoutes(app: Express): Promise { // Legacy single mediaId finalMediaItems = [{ mediaId, displayOrder: 0 }]; } - + // Validate max 10 photos if (finalMediaItems.length > 10) { return res.status(400).json({ error: "Maximum 10 photos autorisées par publication" }); } - + // Validate that stories require media if ((postType === 'story' || postType === 'both') && finalMediaItems.length === 0) { return res.status(400).json({ error: "Les stories nécessitent au moins un média (image ou vidéo)" }); } - + // Security: Verify user has access to all specified pages (unless admin) if (user.role !== 'admin' && pageIds && Array.isArray(pageIds) && pageIds.length > 0) { const accessiblePages = await storage.getUserAccessiblePages(userId); const accessiblePageIds = accessiblePages.map(p => p.id); - - const hasAccessToAllPages = pageIds.every(pageId => + + const hasAccessToAllPages = pageIds.every(pageId => accessiblePageIds.includes(pageId) ); - + if (!hasAccessToAllPages) { - return res.status(403).json({ - error: "Vous n'avez pas accès à certaines pages sélectionnées" + return res.status(403).json({ + error: "Vous n'avez pas accès à certaines pages sélectionnées" }); } } - + // Convert scheduledFor string to Date if provided if (postFields.scheduledFor && typeof postFields.scheduledFor === 'string') { postFields.scheduledFor = new Date(postFields.scheduledFor); } - + // Set status to "scheduled" if scheduledFor is provided, otherwise "draft" if (postFields.scheduledFor) { postFields.status = "scheduled"; - + // Validate that scheduled posts require at least one page if (!pageIds || !Array.isArray(pageIds) || pageIds.length === 0) { return res.status(400).json({ error: "Les posts programmés nécessitent au moins une page cible" }); } } - + // Create the post const postData = insertPostSchema.parse({ ...postFields, userId }); const post = await storage.createPost(postData); - + // Link media to post if provided (with display order) if (finalMediaItems.length > 0) { const postMediaValues = finalMediaItems.map(item => ({ @@ -928,15 +956,15 @@ export async function registerRoutes(app: Express): Promise { })); await db.insert(postMedia).values(postMediaValues); } - + // Create scheduled posts for each selected page if (pageIds && Array.isArray(pageIds) && pageIds.length > 0) { - const scheduledAt = postFields.scheduledFor - ? new Date(postFields.scheduledFor) + const scheduledAt = postFields.scheduledFor + ? new Date(postFields.scheduledFor) : new Date(); // Publish immediately if no date specified - + const finalPostType = postType || 'feed'; - + for (const pageId of pageIds) { // If postType is "both", create two separate scheduled posts (story + feed) if (finalPostType === 'both') { @@ -962,7 +990,7 @@ export async function registerRoutes(app: Express): Promise { } } } - + res.json(post); } catch (error) { console.error("Error creating post:", error); @@ -997,7 +1025,7 @@ export async function registerRoutes(app: Express): Promise { if (scheduledPostsForPost.length > 0) { const accessiblePages = await storage.getUserAccessiblePages(userId); const accessiblePageIds = accessiblePages.map(p => p.id); - + // Vérifier si au moins une page du post est accessible const hasAccess = scheduledPostsForPost.some(sp => accessiblePageIds.includes(sp.pageId)); if (hasAccess) { @@ -1075,19 +1103,19 @@ export async function registerRoutes(app: Express): Promise { const user = req.user as User; const userId = user.id; const { startDate, endDate } = req.query; - + const start = startDate ? new Date(startDate as string) : undefined; const end = endDate ? new Date(endDate as string) : undefined; - + let scheduledPosts; - + if (user.role === 'admin') { // Admin voit tous les posts programmés - on récupère toutes les pages - const allPages = await storage.getAllUsers().then(users => + const allPages = await storage.getAllUsers().then(users => Promise.all(users.map(u => storage.getSocialPages(u.id))) ).then(pagesArrays => pagesArrays.flat()); const allPageIds = allPages.map(p => p.id); - + if (allPageIds.length > 0) { scheduledPosts = await storage.getScheduledPostsByPages(allPageIds, start, end); } else { @@ -1097,14 +1125,14 @@ export async function registerRoutes(app: Express): Promise { // User voit uniquement les posts programmés sur les pages qui lui sont attribuées const accessiblePages = await storage.getUserAccessiblePages(userId); const accessiblePageIds = accessiblePages.map(p => p.id); - + if (accessiblePageIds.length > 0) { scheduledPosts = await storage.getScheduledPostsByPages(accessiblePageIds, start, end); } else { scheduledPosts = []; } } - + res.json(scheduledPosts); } catch (error) { console.error("Error fetching scheduled posts:", error); @@ -1117,23 +1145,23 @@ export async function registerRoutes(app: Express): Promise { const user = req.user as User; const userId = user.id; const { id } = req.params; - + // Verify the scheduled post exists const scheduledPost = await storage.getScheduledPost(id); if (!scheduledPost) { return res.status(404).json({ error: "Scheduled post not found" }); } - + const post = await storage.getPost(scheduledPost.postId); if (!post) { return res.status(404).json({ error: "Post not found" }); } - + // Admin peut tout supprimer, user peut supprimer uniquement ses propres posts if (user.role !== 'admin' && post.userId !== userId) { return res.status(403).json({ error: "Unauthorized" }); } - + await storage.deleteScheduledPost(id); res.json({ success: true }); } catch (error) { @@ -1147,37 +1175,37 @@ export async function registerRoutes(app: Express): Promise { const user = req.user as User; const userId = user.id; const { id } = req.params; - + // Verify the scheduled post exists const scheduledPost = await storage.getScheduledPost(id); if (!scheduledPost) { return res.status(404).json({ error: "Scheduled post not found" }); } - + const post = await storage.getPost(scheduledPost.postId); if (!post) { return res.status(404).json({ error: "Post not found" }); } - + // Admin peut tout modifier, user peut modifier uniquement ses propres posts if (user.role !== 'admin' && post.userId !== userId) { return res.status(403).json({ error: "Unauthorized" }); } - + // Only allow updating scheduledAt and pageId const { scheduledAt, pageId } = req.body; const updateData: Partial = {}; - + if (scheduledAt) { updateData.scheduledAt = new Date(scheduledAt); // Synchroniser avec la table posts await storage.updatePost(scheduledPost.postId, { scheduledFor: new Date(scheduledAt) }); } - + if (pageId) { updateData.pageId = pageId; } - + const updated = await storage.updateScheduledPost(id, updateData); res.json(updated); } catch (error) { @@ -1189,7 +1217,7 @@ export async function registerRoutes(app: Express): Promise { app.post("/api/scheduled-posts", requireAuth, async (req, res) => { try { const scheduledPostData = insertScheduledPostSchema.parse(req.body); - + // If postType is "both", create two separate scheduled posts (story + feed) // This prevents retry loops - each post type is independent if (scheduledPostData.postType === 'both') { @@ -1220,9 +1248,9 @@ export async function registerRoutes(app: Express): Promise { try { const user = req.user as User; const userId = user.id; - + let pages: SocialPage[]; - + if (user.role === 'admin') { // Les admins voient toutes les pages de tous les utilisateurs const allUsers = await storage.getAllUsers(); @@ -1234,7 +1262,7 @@ export async function registerRoutes(app: Express): Promise { // Les utilisateurs normaux voient uniquement les pages auxquelles ils ont accès pages = await storage.getUserAccessiblePages(userId); } - + res.json(pages); } catch (error) { console.error("Error fetching pages:", error); @@ -1246,15 +1274,15 @@ export async function registerRoutes(app: Express): Promise { try { const user = req.user as User; const userId = user.id; - + // Calculate token expiration date (60 days from now) const tokenExpiresAt = new Date(); tokenExpiresAt.setDate(tokenExpiresAt.getDate() + 60); - - const pageData = insertSocialPageSchema.parse({ - ...req.body, + + const pageData = insertSocialPageSchema.parse({ + ...req.body, userId, - tokenExpiresAt + tokenExpiresAt }); const page = await storage.createSocialPage(pageData); res.json(page); @@ -1269,21 +1297,21 @@ export async function registerRoutes(app: Express): Promise { const user = req.user as User; const userId = user.id; const pageId = req.params.id; - + const existingPage = await storage.getSocialPage(pageId); if (!existingPage || existingPage.userId !== userId) { return res.status(404).json({ error: "Page non trouvée" }); } - + let pageData = insertSocialPageSchema.partial().parse(req.body); - + // If accessToken is being updated, recalculate expiration date (60 days from now) if (pageData.accessToken) { const tokenExpiresAt = new Date(); tokenExpiresAt.setDate(tokenExpiresAt.getDate() + 60); pageData = { ...pageData, tokenExpiresAt }; } - + const updatedPage = await storage.updateSocialPage(pageId, pageData); res.json(updatedPage); } catch (error) { @@ -1297,12 +1325,12 @@ export async function registerRoutes(app: Express): Promise { const user = req.user as User; const userId = user.id; const pageId = req.params.id; - + const existingPage = await storage.getSocialPage(pageId); if (!existingPage || existingPage.userId !== userId) { return res.status(404).json({ error: "Page non trouvée" }); } - + await storage.deleteSocialPage(pageId); res.json({ success: true }); } catch (error) { @@ -1330,7 +1358,7 @@ export async function registerRoutes(app: Express): Promise { const user = req.user as User; const userId = user.id; const config = await storage.getCloudinaryConfig(userId); - + if (!config) { return res.json(null); } @@ -1348,10 +1376,10 @@ export async function registerRoutes(app: Express): Promise { try { const user = req.user as User; const userId = user.id; - + // Check if config already exists const existingConfig = await storage.getCloudinaryConfig(userId); - + let config; if (existingConfig) { // Pour les mises à jour, utiliser le schéma qui rend les secrets optionnels @@ -1359,14 +1387,14 @@ export async function registerRoutes(app: Express): Promise { ...req.body, userId, }); - + // Si apiKey/apiSecret ne sont pas fournis, garder les anciens const finalData = { ...updateData, apiKey: updateData.apiKey || existingConfig.apiKey, apiSecret: updateData.apiSecret || existingConfig.apiSecret, }; - + config = await storage.updateCloudinaryConfig(userId, finalData); } else { // Pour les créations, exiger tous les champs @@ -1399,8 +1427,8 @@ export async function registerRoutes(app: Express): Promise { // Check if Cloudinary is configured const cloudinaryConfig = await storage.getCloudinaryConfig(userId); if (!cloudinaryConfig) { - return res.status(400).json({ - error: "Cloudinary not configured. Please configure Cloudinary first." + return res.status(400).json({ + error: "Cloudinary not configured. Please configure Cloudinary first." }); } @@ -1490,7 +1518,7 @@ export async function registerRoutes(app: Express): Promise { const user = req.user as User; const userId = user.id; const config = await storage.getOpenrouterConfig(userId); - + if (!config) { return res.json(null); } @@ -1508,10 +1536,10 @@ export async function registerRoutes(app: Express): Promise { try { const user = req.user as User; const userId = user.id; - + // Check if config already exists const existingConfig = await storage.getOpenrouterConfig(userId); - + let config; if (existingConfig) { // Pour les mises à jour, utiliser le schéma qui rend apiKey optionnel @@ -1519,13 +1547,13 @@ export async function registerRoutes(app: Express): Promise { ...req.body, userId, }); - + // Si apiKey n'est pas fourni, garder l'ancien const finalData = { ...updateData, apiKey: updateData.apiKey || existingConfig.apiKey, }; - + config = await storage.updateOpenrouterConfig(userId, finalData); } else { // Pour les créations, exiger tous les champs diff --git a/server/storage.ts b/server/storage.ts index 65166b4..5713c56 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -1,15 +1,15 @@ -import { - users, - socialPages, - media, - posts, +import { + users, + socialPages, + media, + posts, postMedia, scheduledPosts, aiGenerations, cloudinaryConfig, openrouterConfig, userPagePermissions, - type User, + type User, type InsertUser, type SocialPage, type InsertSocialPage, @@ -31,6 +31,7 @@ import { } from "@shared/schema"; import { db } from "./db"; import { eq, and, gte, lte, desc, asc, isNull, inArray } from "drizzle-orm"; +import { encrypt, decrypt, isEncrypted } from "./utils/encryption"; export interface IStorage { // Users @@ -131,21 +132,43 @@ export class DatabaseStorage implements IStorage { // Social Pages async getSocialPages(userId: string): Promise { - return await db.select().from(socialPages).where(eq(socialPages.userId, userId)); + const pages = await db.select().from(socialPages).where(eq(socialPages.userId, userId)); + // Déchiffrer les tokens pour chaque page + return pages.map(page => ({ + ...page, + accessToken: decrypt(page.accessToken) + })); } async getSocialPage(id: string): Promise { const [page] = await db.select().from(socialPages).where(eq(socialPages.id, id)); + if (page) { + // Déchiffrer le token + page.accessToken = decrypt(page.accessToken); + } return page || undefined; } async createSocialPage(page: InsertSocialPage): Promise { - const [newPage] = await db.insert(socialPages).values(page).returning(); + // Chiffrer le token avant stockage + const encryptedPage = { + ...page, + accessToken: encrypt(page.accessToken) + }; + const [newPage] = await db.insert(socialPages).values(encryptedPage).returning(); + // Retourner avec le token déchiffré + newPage.accessToken = decrypt(newPage.accessToken); return newPage; } async updateSocialPage(id: string, page: Partial): Promise { - const [updated] = await db.update(socialPages).set(page).where(eq(socialPages.id, id)).returning(); + // Chiffrer le token si présent dans la mise à jour + const updateData = page.accessToken + ? { ...page, accessToken: encrypt(page.accessToken) } + : page; + const [updated] = await db.update(socialPages).set(updateData).where(eq(socialPages.id, id)).returning(); + // Retourner avec le token déchiffré + updated.accessToken = decrypt(updated.accessToken); return updated; } @@ -219,7 +242,7 @@ export class DatabaseStorage implements IStorage { async updatePostMedia(postId: string, mediaIds: string[]): Promise { await db.delete(postMedia).where(eq(postMedia.postId, postId)); - + if (mediaIds.length > 0) { const postMediaEntries = mediaIds.map((mediaId, index) => ({ postId, @@ -423,7 +446,7 @@ export class DatabaseStorage implements IStorage { .from(userPagePermissions) .innerJoin(socialPages, eq(userPagePermissions.pageId, socialPages.id)) .where(eq(userPagePermissions.userId, userId)); - + return permissions.map(p => p.social_pages); } } diff --git a/server/utils/encryption.ts b/server/utils/encryption.ts new file mode 100644 index 0000000..f050fcd --- /dev/null +++ b/server/utils/encryption.ts @@ -0,0 +1,92 @@ +import crypto from 'crypto'; + +const ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 16; + +/** + * Récupère la clé de chiffrement depuis les variables d'environnement. + * Dérive une clé de 32 bytes pour AES-256. + */ +function getEncryptionKey(): Buffer { + const key = process.env.ENCRYPTION_KEY; + if (!key) { + // En développement, utiliser une clé par défaut (non sécurisé pour la production) + if (process.env.NODE_ENV !== 'production') { + console.warn('⚠️ ENCRYPTION_KEY non défini. Utilisation d\'une clé par défaut (développement uniquement)'); + return crypto.scryptSync('dev-default-key-not-secure', 'salt', 32); + } + throw new Error('ENCRYPTION_KEY non défini dans les variables d\'environnement'); + } + // Dériver une clé de 32 bytes depuis la clé fournie + return crypto.scryptSync(key, 'socialflow-salt', 32); +} + +/** + * Chiffre une chaîne de texte avec AES-256-GCM. + * @param text - Le texte à chiffrer + * @returns Le texte chiffré au format: iv:authTag:encrypted (hex) + */ +export function encrypt(text: string): string { + const key = getEncryptionKey(); + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + const authTag = cipher.getAuthTag(); + + // Format: iv:authTag:encrypted + return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`; +} + +/** + * Déchiffre une chaîne chiffrée avec AES-256-GCM. + * @param encryptedText - Le texte chiffré au format iv:authTag:encrypted + * @returns Le texte déchiffré + */ +export function decrypt(encryptedText: string): string { + // Si le texte ne contient pas le format attendu, retourner tel quel + // (pour la rétrocompatibilité avec les tokens non chiffrés) + if (!encryptedText.includes(':')) { + return encryptedText; + } + + const key = getEncryptionKey(); + const parts = encryptedText.split(':'); + + if (parts.length !== 3) { + // Format invalide, retourner tel quel (rétrocompatibilité) + return encryptedText; + } + + const [ivHex, authTagHex, encrypted] = parts; + + try { + const iv = Buffer.from(ivHex, 'hex'); + const authTag = Buffer.from(authTagHex, 'hex'); + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(authTag); + + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } catch (error) { + // Si le déchiffrement échoue, retourner tel quel (rétrocompatibilité) + console.warn('⚠️ Échec du déchiffrement, token probablement non chiffré'); + return encryptedText; + } +} + +/** + * Vérifie si un texte est déjà chiffré (format iv:authTag:encrypted). + * @param text - Le texte à vérifier + * @returns true si le texte semble être chiffré + */ +export function isEncrypted(text: string): boolean { + if (!text.includes(':')) return false; + const parts = text.split(':'); + if (parts.length !== 3) return false; + // Vérifier que les parties ressemblent à du hex + return parts.every(part => /^[a-f0-9]+$/i.test(part)); +}