diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..af33bdf --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,145 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Vscode files +.vscode + +# Data +data/* + +# Temporary +tests/ \ No newline at end of file diff --git a/backend/config/config.json b/backend/config/config.json new file mode 100644 index 0000000..a060d02 --- /dev/null +++ b/backend/config/config.json @@ -0,0 +1,19 @@ +{ + "port": 8000, + + "users": { + "admin": { + "username": "admin", + "password": "admin" + } + }, + + "database": { + "type": "sqlite" + }, + + "auth": { + "JWT_secret": "HGF7654EGBNKJNBJH6754356788GJHGY", + "tokenExpiry": "1h" + } +} \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..dc18e1e --- /dev/null +++ b/backend/package.json @@ -0,0 +1,35 @@ +{ + "name": "wf-radio-backend", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "start": "node server.js", + "start-auto": "pnpx nodemon server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.5.2", + "dependencies": { + "ajv": "^8.17.1", + "bcrypt": "^5.1.1", + "better-sqlite3": "^11.9.1", + "dompurify": "^3.2.4", + "express": "^5.1.0", + "jsdom": "^26.0.0", + "jsonwebtoken": "^9.0.2", + "marked": "^15.0.7", + "mysql": "^2.18.1" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "bcrypt", + "better-sqlite3" + ] + }, + "devDependencies": { + "nodemon": "^3.1.9" + } +} diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml new file mode 100644 index 0000000..d1d6f0f --- /dev/null +++ b/backend/pnpm-lock.yaml @@ -0,0 +1,1884 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + ajv: + specifier: ^8.17.1 + version: 8.17.1 + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 + better-sqlite3: + specifier: ^11.9.1 + version: 11.9.1 + dompurify: + specifier: ^3.2.4 + version: 3.2.5 + express: + specifier: ^5.1.0 + version: 5.1.0 + jsdom: + specifier: ^26.0.0 + version: 26.1.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + marked: + specifier: ^15.0.7 + version: 15.0.10 + mysql: + specifier: ^2.18.1 + version: 2.18.1 + devDependencies: + nodemon: + specifier: ^3.1.9 + version: 3.1.10 + +packages: + + '@asamuzakjp/css-color@3.1.4': + resolution: {integrity: sha512-SeuBV4rnjpFNjI8HSgKUwteuFdkHwkboq31HWzznuqgySQir+jSTczoWVVL4jvOjKjuH80fMDG0Fvg1Sb+OJsA==} + + '@csstools/color-helpers@5.0.2': + resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.3': + resolution: {integrity: sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-color-parser@3.0.9': + resolution: {integrity: sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcrypt@5.1.1: + resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} + engines: {node: '>= 10.0.0'} + + better-sqlite3@11.9.1: + resolution: {integrity: sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==} + + bignumber.js@9.0.0: + resolution: {integrity: sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cssstyle@4.3.1: + resolution: {integrity: sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==} + engines: {node: '>=18'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + dompurify@3.2.5: + resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + entities@6.0.0: + resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + inflight@1.0.6: + resolution: {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. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + marked@15.0.10: + resolution: {integrity: sha512-BXzsfFiR2UqXFKRwpugWuCYi9mWd1aX/Yns/X52xWfvfen9lnGEDbJw9ZEjjvLZVqntqT2gX45eYvqb2dIokDw==} + engines: {node: '>= 18'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mysql@2.18.1: + resolution: {integrity: sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==} + engines: {node: '>= 0.6'} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-abi@3.74.0: + resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==} + engines: {node: '>=10'} + + node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + nodemon@3.1.10: + resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==} + engines: {node: '>=10'} + hasBin: true + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + nwsapi@2.2.20: + resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@2.3.7: + resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + sqlstring@2.3.1: + resolution: {integrity: sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tar-fs@2.1.2: + resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.1: + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + +snapshots: + + '@asamuzakjp/css-color@3.1.4': + dependencies: + '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 10.4.3 + + '@csstools/color-helpers@5.0.2': {} + + '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-color-parser@3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/color-helpers': 5.0.2 + '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-tokenizer@3.0.3': {} + + '@mapbox/node-pre-gyp@1.0.11': + dependencies: + detect-libc: 2.0.4 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.7.1 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@types/trusted-types@2.0.7': + optional: true + + abbrev@1.1.1: {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + agent-base@6.0.2: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.3: {} + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + aproba@2.0.0: {} + + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + bcrypt@5.1.1: + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + node-addon-api: 5.1.0 + transitivePeerDependencies: + - encoding + - supports-color + + better-sqlite3@11.9.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bignumber.js@9.0.0: {} + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.0(supports-color@5.5.0) + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-equal-constant-time@1.0.1: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + chownr@2.0.0: {} + + color-support@1.1.3: {} + + concat-map@0.0.1: {} + + console-control-strings@1.1.0: {} + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + core-util-is@1.0.3: {} + + cssstyle@4.3.1: + dependencies: + '@asamuzakjp/css-color': 3.1.4 + rrweb-cssom: 0.8.0 + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.0(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + decimal.js@10.5.0: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + delegates@1.0.0: {} + + depd@2.0.0: {} + + detect-libc@2.0.4: {} + + dompurify@3.2.5: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + entities@6.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + expand-template@2.0.3: {} + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.0(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-uri@3.0.6: {} + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.0: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gauge@3.0.2: + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + gopd@1.2.0: {} + + has-flag@3.0.0: {} + + has-symbols@1.1.0: {} + + has-unicode@2.0.1: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ipaddr.js@1.9.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-promise@4.0.0: {} + + isarray@1.0.0: {} + + jsdom@26.1.0: + dependencies: + cssstyle: 4.3.1 + data-urls: 5.0.0 + decimal.js: 10.5.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.20 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + json-schema-traverse@1.0.0: {} + + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.1 + + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + lru-cache@10.4.3: {} + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + marked@15.0.10: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mimic-response@3.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimist@1.2.8: {} + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp-classic@0.5.3: {} + + mkdirp@1.0.4: {} + + ms@2.1.3: {} + + mysql@2.18.1: + dependencies: + bignumber.js: 9.0.0 + readable-stream: 2.3.7 + safe-buffer: 5.1.2 + sqlstring: 2.3.1 + + napi-build-utils@2.0.0: {} + + negotiator@1.0.0: {} + + node-abi@3.74.0: + dependencies: + semver: 7.7.1 + + node-addon-api@5.1.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + nodemon@3.1.10: + dependencies: + chokidar: 3.6.0 + debug: 4.4.0(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.1 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + + normalize-path@3.0.0: {} + + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + + nwsapi@2.2.20: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parse5@7.3.0: + dependencies: + entities: 6.0.0 + + parseurl@1.3.3: {} + + path-is-absolute@1.0.1: {} + + path-to-regexp@8.2.0: {} + + picomatch@2.3.1: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.0.4 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.74.0 + pump: 3.0.2 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.2 + tunnel-agent: 0.6.0 + + process-nextick-args@2.0.1: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pstree.remy@1.1.8: {} + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@2.3.7: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + require-from-string@2.0.2: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + router@2.2.0: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + + rrweb-cssom@0.8.0: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + semver@6.3.1: {} + + semver@7.7.1: {} + + send@1.2.0: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.1 + + sqlstring@2.3.1: {} + + statuses@2.0.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + symbol-tree@3.2.4: {} + + tar-fs@2.1.2: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.2 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + touch@3.1.1: {} + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@0.0.3: {} + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + undefsafe@2.0.5: {} + + unpipe@1.0.0: {} + + util-deprecate@1.0.2: {} + + vary@1.1.2: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@3.0.1: {} + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + wrappy@1.0.2: {} + + ws@8.18.1: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@4.0.0: {} diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..33b1df6 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,38 @@ +// --- Imports --- + +const express = require("express"); +const app = express(); +const configManager = require("./src/utils/configManager"); +const { connectDatabase, initDatabase } = require('./src/database/index'); + + +// --- Load configuration --- +const config = configManager.loadConfig(); + +// --- Body parsing --- +app.use(express.json()); // Necessary to parse JSON bodies + +// Database connection +(async () => { + + + // --- Database connection --- + await connectDatabase(); + await initDatabase(); + + // --- Routing --- + app.use("/", require("./src/routes/index")); + app.use("/mods", require("./src/routes/mods")); + app.use("/users", require("./src/routes/users")); + app.use("/list", require("./src/routes/list")); + app.use("/login", require("./src/routes/login")); + +})(); + + +// --- Launch --- + +const port = config.port; +app.listen(port, () => { + console.log("Server listening on port " + port + "..."); +}) \ No newline at end of file diff --git a/backend/src/controllers/auth.js b/backend/src/controllers/auth.js new file mode 100644 index 0000000..7bd6571 --- /dev/null +++ b/backend/src/controllers/auth.js @@ -0,0 +1,17 @@ +const handleError = require("../middleware/errors"); +const authService = require("../services/authService"); + +async function login(req, res) { + try { + const username = req.body.username; + const email = req.body.email; + const password = req.body.password + const token = await authService.login(username, email, password); + res.json({ token }); + } catch (err) { + handleError(err, res); + } + +} + +module.exports = { login }; \ No newline at end of file diff --git a/backend/src/controllers/index.js b/backend/src/controllers/index.js new file mode 100644 index 0000000..3580e6b --- /dev/null +++ b/backend/src/controllers/index.js @@ -0,0 +1,14 @@ +const index_service = require("../services/indexService"); +const handleError = require("../middleware/errors"); + + +async function getVersion(req, res) { + try { + const query_result = await index_service.getVersion(); + res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + +module.exports = { getVersion }; \ No newline at end of file diff --git a/backend/src/controllers/modpacks.js b/backend/src/controllers/modpacks.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/controllers/mods.js b/backend/src/controllers/mods.js new file mode 100644 index 0000000..d1f36dc --- /dev/null +++ b/backend/src/controllers/mods.js @@ -0,0 +1,68 @@ +const handleError = require("../middleware/errors"); +const mod_service = require("../services/modService"); +const { authorizeModModification, authenticateToken } = require("../middleware/auth"); + +async function listMods(req, res) { + try { + // Query + const query_result = await mod_service.getAllMods(); + res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + +async function createMod(req, res) { + try { + // Authenticate + await authenticateToken(req); + // Query + const mod_data = req.body; + const user = req.token_infos.username; + const query_result = await mod_service.createMod(mod_data, user); + res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + +async function modifyMod(req, res) { + try { + // Authorize + authorizeModModification(req); + // Query + const mod_data = req.body; + const query_result = await mod_service.modifyMod(mod_data); + res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + +async function getModByName(req, res) { + try { + // Query + const name = req.params.name + const query_result = await mod_service.getModByName(name); + res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + +async function deleteMod(req, res) { + try { + // Authorize + authorizeModModification(req); + // Query + const name = req.params.name + const query_result = await mod_service.deleteMod(name); + res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + + + +module.exports = { listMods, getModByName, createMod, modifyMod, deleteMod }; \ No newline at end of file diff --git a/backend/src/controllers/users.js b/backend/src/controllers/users.js new file mode 100644 index 0000000..71e475f --- /dev/null +++ b/backend/src/controllers/users.js @@ -0,0 +1,63 @@ +const handleError = require("../middleware/errors"); +const user_service = require("../services/userService") +const { authorizeUserModification } = require("../middleware/auth"); + + +async function listUsers(req, res) { + try { + // Query + const query_result = await user_service.getAllUsers(); + res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + +async function getUserByName(req, res) { + try { + // Query + const query_result = await user_service.getUserByName(req.params.name); + res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + +async function createUser(req, res) { + try { + // Query + const query_result = await user_service.createUser(req.body); + res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + +async function modifyUser(req, res) { + try { + // Query + const diff_data = req.body; + const query_result = await user_service.modifyUser(diff_data); + res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + +async function deleteUser(req, res) { + try { + // Authenticate + await authorizeUserModification(req); + // Query + const user = req.params.name; + const token_user = req.token_infos + const query_result = await user_service.deleteUser(user, token_user); + return res.json(query_result); + } catch (error) { + handleError(error, res); + } +} + + + +module.exports = { listUsers, getUserByName, createUser, modifyUser, deleteUser }; \ No newline at end of file diff --git a/backend/src/database/index.js b/backend/src/database/index.js new file mode 100644 index 0000000..1399795 --- /dev/null +++ b/backend/src/database/index.js @@ -0,0 +1,112 @@ +const MySQLDatabase = require("./mysql"); +const SQLiteDatabase = require("./sqlite"); +const { getConfig } = require("../utils/configManager"); + +let db; + +async function connectDatabase() { + + // Get config + const config = await getConfig(); + + // Choose database type + if (config.database.type === "mysql") { + db = new MySQLDatabase(config.database); + } else if (config.database.type === "sqlite") { + db = new SQLiteDatabase(config.database); + } else { + throw new Error("Invalid database type: ", config.database.type); + } + + // Connect + await db.connect(); + return db; +} + + +// Setups the database by creating the tables and the default objects +async function initDatabase() { + + if (!db) { + throw new Error("Database is not connected"); + } + + // --- Users --- + + // Uers table + db.exec(`CREATE TABLE IF NOT EXISTS Users ( + username TINYTEXT PRIMARY KEY, + display_name TINYTEXT NOT NULL, + email TINYTEXT NOT NULL, + password TINYTEXT NOT NULL, + profile_picture LONGTEXT, + role TINYTEXT NOT NULL, + settings LONGTEXT + );`); + + // --- Mods --- + + // Mods table + db.exec(`CREATE TABLE IF NOT EXISTS Mods ( + name TINYTEXT PRIMARY KEY, + display_name TINYTEXT NOT NULL, + author TINYTEXT NOT NULL, + description TINYTEXT NOT NULL, + + FOREIGN KEY (author) REFERENCES Users(username) + );`); + + // Mods complementary infos + db.exec(`CREATE TABLE IF NOT EXISTS ModInfos ( + mod TINYTEXT PRIMARY KEY, + full_description TEXT NOT NULL, + license TINYTEXT, + custom_license TEXT, + links TEXT, + creation_date TINYTEXT NOT NULL, + downloads_count INT NOT NULL, + + FOREIGN KEY (mod) REFERENCES Mods(name) + );`); + + // Mods tags + db.exec(`CREATE TABLE IF NOT EXISTS ModTags ( + mod TINYTEXT NOT NULL, + tag TINYTEXT NOT NULL, + + FOREIGN KEY (mod) REFERENCES Mods(name) + );`); + + // Mods versions + db.exec(`CREATE TABLE IF NOT EXISTS ModVersions ( + mod TINYTEXT NOT NULL, + version_number TINYTEXT NOT NULL, + channel TINYTEXT NOT NULL, + changelog TEXT NOT NULL, + release_date TINYTEXT NOT NULL, + game_version TINYTEXT NOT NULL, + platform TINYTEXT NOT NULL, + environment TINYTEXT NOT NULL, + url TINYTEXT NOT NULL, + + FOREIGN KEY (mod) REFERENCES Mods(name) + );`); + + // User favorites (mods) + db.exec(`CREATE TABLE IF NOT EXISTS UserFavoriteMods ( + username TINYTEXT NOT NULL, + mod TINYTEXT NOT NULL, + + FOREIGN KEY (username) REFERENCES Users(username), + FOREIGN KEY (mod) REFERENCES Mods(name) + );`); + +} + + +function getDatabase() { + return db; +} + + +module.exports = { getDatabase, connectDatabase, initDatabase }; \ No newline at end of file diff --git a/backend/src/database/mysql.js b/backend/src/database/mysql.js new file mode 100644 index 0000000..df96f30 --- /dev/null +++ b/backend/src/database/mysql.js @@ -0,0 +1,30 @@ +class MySQLDatabase { + constructor(config) { + const mysql = require("mysql2/promise"); + this.config = config; + } + + + async connect() { + this.db = await mysql.createConnection({ + host: this.config.host, + user: this.config.user, + password: this.config.password, + database: this.config.database, + }); + console.log("Connected to MySQL"); + } + + async close() { + await this.db.end(); + } + + async query(sql, params) { + throw new Error("Not implemented"); //TODO + const [results] = await this.db.execute(sql, params); + return results; + } +} + + +module.exports = MySQLDatabase; \ No newline at end of file diff --git a/backend/src/database/sqlite.js b/backend/src/database/sqlite.js new file mode 100644 index 0000000..b81e219 --- /dev/null +++ b/backend/src/database/sqlite.js @@ -0,0 +1,70 @@ +const sqlite = require("better-sqlite3"); + +class SQLiteDatabase { + + constructor(config) { + this.config = config; + this.db = null; + } + + async connect() { + try { + this.db = new sqlite("./data/sqlite.db"); + // this.db.pragma("journal_mode = WAL"); + console.log("Connected to SQLite"); + } catch (err) { + console.error("Error connecting to SQLite database: ", err); + process.exit(1); + } + } + + async close() { + if (this.db && this.db.open) { + this.db.close(); + console.debug("Closed database connection"); + } + } + + async query(sql, params = []) { + try { + if (params.length > 0) { + return this.db.prepare(sql).all(params); + } else { + return this.db.prepare(sql).all(); + } + } catch (err) { + console.error("Error executing prepared query:", err); + throw err; + } + } + + async exec(sql) { + try { + return this.db.exec(sql); + } catch (err) { + console.error("Error executing statement:", err)} + + } + + async prepare(sql, params = []) { + try { + if (params.length > 0) { + return this.db.prepare(sql).run(params); + } else { + return this.db.prepare(sql); + } + } catch (err) { + console.error("Error executing prepared statement:", err)} + } + + async exists(table, attribute, value) { + try { + return this.db.prepare(`SELECT COUNT(*) FROM ${table} WHERE ${attribute} = ?`).get(value)['COUNT(*)'] > 0; + } catch (err) { + console.error("Error checking item existence"); + } + } + +} + +module.exports = SQLiteDatabase; \ No newline at end of file diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js new file mode 100644 index 0000000..d43e385 --- /dev/null +++ b/backend/src/middleware/auth.js @@ -0,0 +1,85 @@ +const { getModByName } = require("../services/modService"); +const { getModpackByName } = require("../services/modpackService"); +const { getUserByName } = require("../services/userService"); +const { verifyToken } = require("../utils/crypto"); +const AppError = require("../utils/appError"); + + +async function authenticateToken(req) { + + const token = req.header("Authorization"); + + if (!token) { + throw new AppError(401, "Missing authorization header", "Unauthorized"); + } + + try { + req.token_infos = await verifyToken(token); + console.debug("Authorizing token from", req.token_infos); + } catch (err) { + throw new AppError(403, "Forbidden: Error verifying the authorization token"); + } + + return req.token_infos; +} + + +async function authorizeModModification(req) { + + // Auth token + await authenticateToken(req); + // Get mod infos + if (!req.params || !req.params.name) { + throw new AppError(400, "No mod name was scpecified", "Bad request"); + } + const mod_name = req.params.name; + const mod = await getModByName(mod_name); + if (!mod) { + throw new AppError(404, "No mod was found with this name", "Not found"); + } + // Authorize + if ( mod.author != req.token_infos.username) { + throw new AppError(401, "Mod author differs from current user", "Unauthorized"); + } +} + +async function authorizeModpackModification(req) { + + // Auth token + await authenticateToken(req); + // Get mod infos + if (!req.params || !req.params.name) { + throw new AppError(400, "No mod name was scpecified", "Bad request"); + } + const modpack_name = req.params.name; + const modpack = await getModpackByName(modpack_name); + if (!modpack) { + throw new AppError(404, "No mod was found with this name", "Not found"); + } + // Authorize + if ( modpack.author != req.token_infos.username) { + throw new AppError(401, "Mod author differs from current user", "Unauthorized"); + } +} + +async function authorizeUserModification(req) { + + // Auth token + await authenticateToken(req); + // Get mod infos + if (!req.params || !req.params.name) { + throw new AppError(400, "No mod name was scpecified", "Bad request"); + } + const user_name = req.params.name; + const user = await getUserByName(user_name); + if (!user) { + throw new AppError(404, "No user was found with this name", "Not found"); + } + // Authorize + if ( user.username != req.token_infos.username) { + throw new AppError(401, "User to modify differs from current user", "Unauthorized"); + } +} + + +module.exports = { authenticateToken, authorizeModModification, authorizeModpackModification, authorizeUserModification }; \ No newline at end of file diff --git a/backend/src/middleware/errors.js b/backend/src/middleware/errors.js new file mode 100644 index 0000000..f51e57a --- /dev/null +++ b/backend/src/middleware/errors.js @@ -0,0 +1,31 @@ +const AppError = require("../utils/appError"); + +const handleError = (err, res) => { + + // Send error infos + if (err instanceof AppError) { + + // Log + if (err.statusCode == 500) { + console.error("Error:", err.message); + if (err.debugMsg) { + console.debug(" >", err.debugMsg); + } + } + + // Response + return res.status(err.statusCode).json({ + status: err.status, + message: err.message + }); + } + + // Default error + console.error("Error:", err.message); + res.status(500).json({ + message: 'Internal server error', + status: 500 + }); +} + +module.exports = handleError; \ No newline at end of file diff --git a/backend/src/models/index.js b/backend/src/models/index.js new file mode 100644 index 0000000..7b8c71c --- /dev/null +++ b/backend/src/models/index.js @@ -0,0 +1,12 @@ +const configManager = require("../utils/configManager"); + +async function getVersion() { + + const version = await configManager.getVersion(); + const res = { + version: version + }; + return res; +} + +module.exports = { getVersion } \ No newline at end of file diff --git a/backend/src/models/mod.js b/backend/src/models/mod.js new file mode 100644 index 0000000..f7abfd9 --- /dev/null +++ b/backend/src/models/mod.js @@ -0,0 +1,192 @@ +const { getDatabase } = require('../database/index'); +const AppError = require('../utils/appError'); +const db = getDatabase(); + + +// --- Get --- + +async function getAllMods() { + return await db.query("SELECT name, display_name, author, description FROM Mods"); +} + +async function getModByName(name) { + return await db.query("SELECT name, display_name, author FROM Mods WHERE name = ?;", [name]); + +} + +async function getModFullInfos(name) { + + // Query + const base_infos = db.query(`SELECT * FROM Mods WHERE name = ?`, [name]); + const other_infos = db.query(`SELECT full_description, license, links, creation_date, downloads_count + FROM ModInfos WHERE name = ?`, [name]); + const tags = getModTags(name); + + // Merge + const res = {...await base_infos, ...await other_infos, ...tags}; + + return res; +} + +async function listVersions(mod_name) { + return await db.query("SELECT * FROM ModVersions WHERE mod = ?", [mod_name]); +} + +async function getVersionByNumber(mod_name, version_number) { + return await db.query(`SELECT * FROM ModVersions + WHERE mod = ? + AND version_number = ?;`, + [mod_name, version_number]); +} + +async function getVersion(mod_name, version_number, game_version, platform, environment) { + return await db.query(`SELECT * FROM ModVersions + WHERE mod = ? + AND version_number = ? + AND game_version = ? + AND platform = ? + AND environment = ?;`, + [mod_name, version_number, game_version, platform, environment]); +} + +// --- Create --- + +async function createMod(name, display_name, author, description, mod_infos) { + + // Extract infos + const { full_description, license, links, creation_date, tags } = mod_infos; + + // Mods table + await db.prepare("INSERT INTO Mods (name, display_name, author, description) \ + VALUES (?, ?, ?, ?)", + [name, display_name, author, description]); + + // ModInfos table + await db.prepare(`INSERT INTO ModInfos (mod, full_description, license, links, creation_date, downloads_count) + VALUES (?, ?, ?, ?, ?, ?)`, + [name, full_description, license.type, links.toString(), creation_date, 0]); + + // Tags + if (tags) { + const tags_proc = addTags(name, tags, []); + } + + + // License + if (license.type == "custom") { + await db.prepare(`UPDATE ModInfos SET custom_license = ? + WHERE mod = ?`, + [license.content, name]); + } + + // Await + if (tags) { + await tags_proc; + } + + return; +} + +async function addVersion(mod, version_number, channel, changelog, release_date, game_version, platform, environment, url) { + + await db.prepare(`INSERT INTO ModVersions (mod, version_number, channel, changelog, release_date, game_version, environment, platform, url) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`, + [mod, version_number, channel, changelog, release_date, game_version, environment, platform, url]); + return; +} + +async function addTags(mod, tags) { + // Add asynchronously + const promises = tags.map(async (tag) => { + db.query(`INSERT INTO ModTags (mod, tag) + VALUES (?, ?);`, + [mod, tag]); + }); + await Promise.all(promises); + + return; +} + + +// --- Update --- + +async function updateMod(name, display_name, author, description) { + + if (display_name) { + await updateModAttributes(name, "display_name", display_name); + } + + if (author) { + await updateModAttribute(nale, "author", author); + } + + if (description) { + await updateModAttribute(name, "description", description) + } +} + + +// --- Delete --- + +async function deleteMod(name) { + await db.prepare("DELETE FROM Mods WHERE name = ?", [name]); + return; +} + +async function deleteVersion(name, version_number, game_version, platform, environment) { + await db.prepare(`DELETE FROM ModVersions WHERE mod = ? + AND version_number = ? + AND game_version = ? + AND platform = ? + AND environment = ?;`, + [name, version_number, game_version, platform, environment]); + return; +} + +async function deleteTags(mod, tags) { + // Remove asynchronously + const promises = tags.map(async (tag) => { + db.query(`DELETE FROM ModTags + WHERE mod = ? AND tag = ?;`, [mod, tag]); + }); + await Promise.all(promises); + + return; +} + + +// --- Utils --- + +async function updateModAttribute(name, attribute, value) { + await db.prepare(`UPDATE Mods SET ${attribute} = ? WHERE name = ?`, [value, name]); + return; +} + +async function updateModInfosAttribute(name, attribute, value) { + await db.prepare(`UPDATE ModInfos SET ${attribute} = ? WHERE name = ?`, [value, name]); + return; +} + +async function exists(name) { + return db.exists("Mods", "name", name); +} + +async function containsVersion(name, version_number, game_version, platform, environment) { + throw new AppError(501, "Not implemented"); + // return db.exists("Mods", "name", name); +} + +async function containsTag(name, tag) { + throw new AppError(501, "Not implemented"); + // return db.exists("Mods", "name", name); +} + + +// --- Exports --- + +module.exports = { getAllMods, getModByName, getModFullInfos, + listVersions, getVersionByNumber, getVersion, + createMod, addVersion, addTags, + updateMod, + deleteMod, deleteVersion, deleteTags, + exists }; \ No newline at end of file diff --git a/backend/src/models/modpack.js b/backend/src/models/modpack.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/models/user.js b/backend/src/models/user.js new file mode 100644 index 0000000..fc66eda --- /dev/null +++ b/backend/src/models/user.js @@ -0,0 +1,125 @@ +const { getDatabase } = require('../database/index'); +const AppError = require('../utils/appError'); +const db = getDatabase(); + + +// --- Get --- + +async function getAllUsers() { + return db.query("SELECT username, display_name, email, profile_picture FROM Users"); +} + +async function getUserByName(name) { + return await db.query("SELECT username, display_name, profile_picture, role FROM Users WHERE username = ?;", [name]); +} + +async function getUserByEmail(email) { + return await db.query("SELECT email, username FROM Users WHERE email = ?;", [email]); +} + +async function getFullUserInfos(name) { + return await db.query("SELECT username, display_name, email, profile_picture, role, settings FROM Users WHERE username = ?;", [name]); +} + +async function getUserPassword(name) { + return await db.query("SELECT username, password FROM Users WHERE username = ?;", [name]); +} + +async function exists(name) { + return await db.exists("Users", "username", name); +} + + +// --- Create --- + +async function createUser( username, email, password, displayName, profilePicture, settings ) { + + // Create user + await db.prepare(`INSERT INTO Users (username, email, password, display_name, role ) + VALUES (?, ?, ?, ?, ? )`, + [username, email, password, displayName, "user"]); + + // Handle nullable fields + if (profilePicture) { + await updateUserAttribute(username, "profile_picture", profilePicture); + } + + if (settings) { + await updateUserAttribute(username, "settings", settings); + } + return; +} + +async function addFavoriteMods(username, favs) { + + const promises = favs.map(async (mod) => { + db.query(`INSERT INTO UserFavoriteMods + (username, mod) VALUES (?, ?);`, + [username, mod]); + + }); + await Promise.all(promises); + + return; +} + + +// --- Update --- + +async function updateUser(username, display_name, email, profile_picture, settings) { + + if (display_name) { + await updateUserAttribute(username, "display_name", display_name); + } + + if (email) { + await updateUserAttribute(username, "email", email); + } + + if (profile_picture) { + await updateUserAttribute(username, "profile_picture", profile_picture) + } + + if (settings) { + await updateUserAttribute(username, "settings", settings); + } +} + +async function updateUserPassword(username, password) { + await db.prepare(`UPDATE Users SET password = ? WHERE username = ?`, [password, username]); +} + +async function updateUserAttribute(username, attribute, value) { + await db.prepare(`UPDATE Users SET ${attribute} = ? WHERE username = ?`, [value, username]); + return; +} + + +// --- Delete --- + +async function deleteUser(username) { + await db.prepare("DELETE FROM Users WHERE username = ?", [username]); + return; +} + +async function deleteFavoriteMods(username, favs) { + + const promises = favs.map(async (mod) => { + db.query(`DELETE FROM UserFavoriteMods + WHERE username = ? AND mod = ?;`, [username, mod]); + }); + + // Await + await Promise.all(promises); + + return; +} + + +// --- Exports --- + +module.exports = { getAllUsers, getUserByName, getUserByEmail, getFullUserInfos, getUserPassword, + createUser, addFavoriteMods, + updateUser, + deleteUser, deleteFavoriteMods, + exists } \ No newline at end of file diff --git a/backend/src/routes/index.js b/backend/src/routes/index.js new file mode 100644 index 0000000..36880b8 --- /dev/null +++ b/backend/src/routes/index.js @@ -0,0 +1,11 @@ +const express = require("express"); +const controller = require("../controllers/index"); + +const router = express.Router(); + + +router.get('/version', async (res, req) => { + controller.getVersion(res, req); +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/list.js b/backend/src/routes/list.js new file mode 100644 index 0000000..2829954 --- /dev/null +++ b/backend/src/routes/list.js @@ -0,0 +1,24 @@ +const express = require("express"); +const { listMods } = require("../controllers/mods"); +// const { listModpacks } = require("../controllers/modpacks"); +const { listUsers } = require("../controllers/users"); + +const router = express.Router(); + +// List mods +router.get("/mods", async (req,res) => { + listMods(req, res); +}); + +// List modpacks +// router.get("/modpacks", async (req,res) => { +// listModpacks(req, res); +// }); + +// List users +router.get("/users", async (req,res) => { + listUsers(req, res); +}); + + +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/login.js b/backend/src/routes/login.js new file mode 100644 index 0000000..14b8a87 --- /dev/null +++ b/backend/src/routes/login.js @@ -0,0 +1,11 @@ +const express = require("express"); +const controller = require("../controllers/auth"); + +const router = express.Router(); + +// Login +router.post("/", async (req, res) => { + controller.login(req, res); +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/modpacks.js b/backend/src/routes/modpacks.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/routes/mods.js b/backend/src/routes/mods.js new file mode 100644 index 0000000..82c1e02 --- /dev/null +++ b/backend/src/routes/mods.js @@ -0,0 +1,27 @@ +const express = require("express"); +const controller = require("../controllers/mods"); + +const router = express.Router(); + +// Create a mod +router.post("/", async (req, res) => { + controller.createMod(req, res); +}); + +// Modify mod +router.put("/:name", async (req,res) => { + controller.modifyMod(req,res); +}); + +// Get mod infos +router.get("/:name", async (req,res) => { + controller.getModByName(req, res); +}); + +// Delete mod +router.delete("/:name", async (req,res) => { + controller.deleteMod(req, res); +}); + + +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js new file mode 100644 index 0000000..390451f --- /dev/null +++ b/backend/src/routes/users.js @@ -0,0 +1,27 @@ +const express = require("express"); +const controller = require("../controllers/users"); + +const router = express.Router(); + +// List users +router.get("/", async (req,res) => { + controller.listUsers(req,res); +}); + +// Create a user +router.post("/", async (req, res) => { + controller.createUser(req, res); +}) + +// Get user infos +router.get("/:name", async (req,res) => { + controller.getUserByName(req, res); +}); + +// Delete user +router.delete("/:name", async (req,res) => { + controller.deleteUser(req, res); +}); + + +module.exports = router; \ No newline at end of file diff --git a/backend/src/schemas/auth.js b/backend/src/schemas/auth.js new file mode 100644 index 0000000..e57a75c --- /dev/null +++ b/backend/src/schemas/auth.js @@ -0,0 +1,21 @@ +const Ajv = require("ajv"); +const ajv = new Ajv(); + +// --- Schemas --- + +const AuthUserSchema = { + type: 'object', + properties: { + email: { type: 'string', format: 'email' }, + password: { type: 'string', minLength: 3, maxLength: 30 }, + }, + required: ['email', 'password'], + additionalProperties: false +}; + +const validateAuthUserData = ajv.compile(AuthUserSchema); + + +// --- Exports --- + +module.exports = { validateAuthUserData, validateAuthNodeData }; \ No newline at end of file diff --git a/backend/src/schemas/mod.js b/backend/src/schemas/mod.js new file mode 100644 index 0000000..6d7dc9d --- /dev/null +++ b/backend/src/schemas/mod.js @@ -0,0 +1,21 @@ +const Ajv = require("ajv"); +const ajv = new Ajv(); + +// --- Schemas --- +//TODO + +const newModSchema = { + type: 'object', + properties: { + name: { type: 'string'}, + }, + required: ['name'], + additionalProperties: false +}; + +const validateNewModData = ajv.compile(newModSchema); + + +// --- Exports --- + +module.exports = { validateNewModData }; \ No newline at end of file diff --git a/backend/src/schemas/modpack.js b/backend/src/schemas/modpack.js new file mode 100644 index 0000000..df4a49a --- /dev/null +++ b/backend/src/schemas/modpack.js @@ -0,0 +1,21 @@ +const Ajv = require("ajv"); +const ajv = new Ajv(); + +// --- Schemas --- +//TODO + +const newModpackSchema = { + type: 'object', + properties: { + name: { type: 'string'}, + }, + required: ['name'], + additionalProperties: false +}; + +const validateNewModpackData = ajv.compile(newModpackSchema); + + +// --- Exports --- + +module.exports = { validateNewModpackData }; \ No newline at end of file diff --git a/backend/src/schemas/user.js b/backend/src/schemas/user.js new file mode 100644 index 0000000..0c009a1 --- /dev/null +++ b/backend/src/schemas/user.js @@ -0,0 +1,23 @@ +const Ajv = require("ajv"); +const ajv = new Ajv(); + +// --- Schemas --- +//TODO + +const newUserSchema = { + type: 'object', + properties: { + email: { type: 'string', format: 'email' }, + name: { type: 'string' }, + password: { type: 'string', minLength: 3, maxLength: 30 }, + }, + required: ['name', 'email', 'password'], + additionalProperties: false +}; + +const validateNewUserData = ajv.compile(newUserSchema); + + +// --- Exports --- + +module.exports = { validateNewUserData }; \ No newline at end of file diff --git a/backend/src/services/authService.js b/backend/src/services/authService.js new file mode 100644 index 0000000..e9dee2b --- /dev/null +++ b/backend/src/services/authService.js @@ -0,0 +1,85 @@ +const userModel = require("../models/user"); +const AppError = require("../utils/appError"); +const cryptoUtils = require("../utils/crypto"); +const configManager = require("../utils/configManager"); +const validate = require("../utils/validate_legacy"); + +const JWT_Secret = configManager.getJWTSecret(); + +async function login(username, email, password) { + + // Check for null + if (!(username || email) || !password) { + throw new AppError(400, "Bad request", "missing credentials"); + } + + // Get user data + let user_get; + if (email) { // If matches email + user_get = await userModel.getUserByEmail(email); + } else + if (username) { // if matches username + user_get = await userModel.getUserByName(username); + } else { + console.debug("Failed finding user, weird...") + throw new AppError(401, "Unauthorized", "Invalid credentials"); + } + + // Check if user exists + if (!user_get || user_get.length == 0) { + // throw new AppError(401, "Unauthorized: No user with this name"); + throw new AppError(401, "Unauthorized", "Invalid credentials"); + } + // Just in case + if (user_get.length > 1) { + throw new AppError(500, "Internal server error", "Found multiple users with this name or email, please contact administration"); + } + + const user = user_get[0]; + + // Get user password + const saved_password_get = await userModel.getUserPassword(user.username); + // Check if retrieved password sucessfully + if (!saved_password_get || saved_password_get.length == 0) { + throw new AppError(500, "Unable to retrieve user password"); + } + saved_password = saved_password_get[0].password; + // Check if retrieved password sucessfully again + if (!saved_password) { + throw new AppError(500, "Unable to retrieve user password"); + } + + // Check if passwords match + const passwords_match = await cryptoUtils.passwordsMatch(password, saved_password) + if (!passwords_match) { + // throw new AppError(401, "Unauthorized: Invalid password"); + console.debug(password, "differs from", saved_password); + throw new AppError(401, "Unauthorized", "Invalid credentials"); + } + + const payload = { type: "user", + username: user.username, + email: user.email, + role: user.role }; + + const token = await cryptoUtils.signToken(payload); + return token; + + // // Check if passwords match + // const passwords_match = await bcrypt.compare(password, user[0].password); + // if (!passwords_match) { + // // throw new AppError(401, "Unauthorized: Invalid password"); + // console.debug("Password doesn't match") + // throw new AppError(401, "Unauthorized", "Invalid credentials"); + // } + + // return jwt.sign({ username: user[0].username, role: user[0].role }, await JWT_Secret); +} + +// function authorizeRole(user, roles) { +// if (!user || !roles.includes(user.role)) { +// throw new AppError(401, "Unauthorized: You don't have the necessary permissions to access this resource"); +// } +// } + +module.exports = { login }; \ No newline at end of file diff --git a/backend/src/services/indexService.js b/backend/src/services/indexService.js new file mode 100644 index 0000000..d2fc506 --- /dev/null +++ b/backend/src/services/indexService.js @@ -0,0 +1,7 @@ +const model = require("../models/index"); + +async function getVersion() { + return model.getVersion(); +} + +module.exports = { getVersion } diff --git a/backend/src/services/modService.js b/backend/src/services/modService.js new file mode 100644 index 0000000..4cd8862 --- /dev/null +++ b/backend/src/services/modService.js @@ -0,0 +1,164 @@ +const model = require("../models/mod"); +const AppError = require("../utils/appError"); +const { validateModData } = require("../utils/validate_legacy"); +const { mdToHtml } = require("../utils/convert"); +const { sanitizeModData } = require("../utils/sanitize"); + + +// --- Get --- + +async function getAllMods() { + return model.getAllMods(); +} + +async function getModByName(name) { + const res = model.getModByName(name); + if (res.length == 0) { + throw new AppError(404, "Cannot find mod with this name", "Not found"); + } + return res[0]; +} + +async function getFullModInfos(name) { + const res = model.getFullModInfos(name); + if (res.length == 0) { + throw new AppError(404, "Cannot find mod with this name", "Not found"); + } + return res[0]; +} + +async function getModVersion(infos) { + const { mod, version_number, game_version, platform, environment} = infos; + const res = model.getModVersion(mod, version_number, game_version, platform, environment); + if (res.length == 0) { + throw new AppError(404, "Cannot find mod with this name", "Not found"); + } + return res[0]; +} + + +// --- Create --- + +async function createMod(mod_data, author) { + + // Check body validity + //TODO + console.warn("Skipping validity checks for createMod"); + // await validateModData(mod_data); + + // Generate data + const { name, display_name, description, mod_infos } = mod_data; + mod_infos.full_description = await mdToHtml(mod_infos.full_description); // Convert + await sanitizeModData(mod_data); // Sanitize + //TODO + mod_infos.creation_date = 0 + + // Write changes to database + await model.createMod(name, display_name, author, description, mod_infos); + + // Return + return getModByName(name); +} + +async function addVersion(version_data) { + + // Validate + //TODO + console.warn("Skipping validity checks for addVersion"); + + // Generate data + const { mod_name, version_number, channel, changelog, game_version, + platform, environment, url } = version_data; // Split + changelog = await mdToHtml(changelog); // Convert + await sanitizeModData(mod_data); // Sanitize + const release_date = (new Date()).toLocaleDateString(); + + // Write changes + await model.addVersion(mod_name, version_number, channel, changelog, + release_date, game_version, platform, environment, url); // Database + + // Return + return await model.getModVersion(mod_name, version_number, game_version, platform, environment ); +} + +async function addTags(mod, tags) { + + // Validate + //TODO + console.warn("Skipping validity checks for addTags"); + + // Write changes + await model.addTags(mod, tags); + + // Return + const { tags:res } = await model.getFullModInfos(mod); + return { "mod": mod, "tags": res}; + +} + +// --- Update --- + +async function updateMod(diff_data) { + //TODO + throw new AppError(501, "Not implemented"); +} + + + +// Delete + +async function deleteMod(name) { + + // Check existence + const mod = await model.getModByName(name); + if (!mod) { + throw new AppError(404, "No was found with this name", "Not found") + } + + // Authorize + // TODO move outside of this function + if (mod.author != mod.user) { + throw new AppError(403, "You don't have the necessary permissions to execute this action", "Forbidden"); + } + + // Write changes to database + await model.deleteMod(name); + + // Return + return mod; +} + +async function deleteVersion(version_infos) { + + // Validate + // TODO + + // Generate data + const res = await getModVersion(version_infos); + const { mod, version_number, game_version, platform, environment} = version_infos; + + // Write changes to db + await model.deleteVersion(mod, version_number, game_version, platform, environment); + + // Return + return res; +} + +async function deleteTags(mod, tags) { + + // Validate (check existence) + //TODO + console.warn("Skipping validity checks for deleteTags"); + + // Wites changes to db + await model.deleteTags(mod, tags); + + // Return + const { tags:res } = await model.getFullModInfos(mod); + return { "mod": mod, "tags": res}; +} + +module.exports = { getAllMods, getModByName, getFullModInfos, + createMod, addTags, addVersion, + updateMod, + deleteMod, deleteTags, deleteVersion }; \ No newline at end of file diff --git a/backend/src/services/modpackService.js b/backend/src/services/modpackService.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/services/userService.js b/backend/src/services/userService.js new file mode 100644 index 0000000..8020e25 --- /dev/null +++ b/backend/src/services/userService.js @@ -0,0 +1,46 @@ +const model = require("../models/user"); +const AppError = require("../utils/appError"); +const cryptoUtils = require("../utils/crypto"); +const { validateUserData } = require("../utils/validate_legacy"); +const { sanitizeUserData } = require("../utils/sanitize"); + +async function getAllUsers() { + return await model.getAllUsers(); +} + +async function getUserByName(name) { + const res = await model.getUserByName(name); + return res[0]; +} + +async function createUser(user_data) { + + // Check body validity + // TODO + + // Sanitize + // TODO + + // Gather data + const { username, email, password, display_name, profile_picture, settings } = user_data + const password_hash = await cryptoUtils.hashPassword(password); + + await model.createUser(username, email, password_hash, display_name, null, null); + return model.getUserByName(username); +} + +async function deleteUser(name, token_user) { + + // Check existence + const exists = await model.exists(name); + if (!exists) { + throw new AppError(404, "Cannot find user with this name", "Not found"); + } + + const res = await model.getUserByName(name); + await model.deleteUser(name); + + return res; +} + +module.exports = { getAllUsers, getUserByName, createUser, deleteUser }; \ No newline at end of file diff --git a/backend/src/utils/appError.js b/backend/src/utils/appError.js new file mode 100644 index 0000000..5246dbc --- /dev/null +++ b/backend/src/utils/appError.js @@ -0,0 +1,27 @@ +class AppError extends Error { + constructor(statusCode, message, status = "", debugMsg = "") { + super(message); + this.statusCode = statusCode; + this.debugMsg = debugMsg; + // Get status + if (status === "") { + if (statusCode.toString().startsWith("4")) { + this.status = "Fail"; + } else { + this.status = "Error"; + } + } else { + this.status = status; + } + } +} + +exports.tryCatch = (controller) => async (req, res, next) => { + try { + await controller(req, res, next); + } catch(err) { + next(err); + } +} + +module.exports = AppError; \ No newline at end of file diff --git a/backend/src/utils/configManager.js b/backend/src/utils/configManager.js new file mode 100644 index 0000000..5017a6f --- /dev/null +++ b/backend/src/utils/configManager.js @@ -0,0 +1,89 @@ +// --- Define constants --- + +// Imports +const fs = require("fs"); +const path = require("path"); +const { version } = require("../../package.json"); + + +// Var decalaration +const config_folder = "config"; +const config_file_name = "config.json" + +// Global variables +let config = {}; + +// --- Default config --- + +const default_config = { + + "port": 8000, + + "users": { + "admin": { + "username": "admin", + "password": "admin" + } + }, + + "database": { + "type": "sqlite" + }, + + "auth" : { + "JWT_secret": "HGF7654EGBNKJNBJH6754356788GJHGY", + "tokenExpiry": "1h" + } +} + + +// --- Functions --- + +function loadConfig() { + + let user_config; + + // Parse + try { + // Get user config + user_config = JSON.parse(fs.readFileSync(path.resolve(path.join(config_folder, config_file_name)))); + + // Warns + if (!user_config.auth || !user_config.auth.JWT_secret) { + console.warn("WARNING: No JWT secret provided, using the default one. Please note that using the default secret is a major security risk.") + } + + // Merge default and user configs (default values) + config = { ...default_config, ...user_config }; + } + catch (err) { + // Error messages + console.debug("Error:", err) + console.error("Error loading configuration, using the default settings"); + console.debug("Search path:", path.resolve("./")); + console.debug("Config file:", path.resolve(path.join(config_folder, config_file_name))) + + config = default_config; + } + + return config; + +} + + +async function getConfig() { + return config; +} + +async function getJWTSecret() { + return config.auth.JWT_secret || process.env.JWT_secret; +} + +async function getVersion() { + // Could be done with process.env.npm_package_version + // but may not work without without npm + return version; +} + +// Exports +module.exports = { loadConfig, getConfig, getVersion, getJWTSecret }; \ No newline at end of file diff --git a/backend/src/utils/convert.js b/backend/src/utils/convert.js new file mode 100644 index 0000000..39a56a9 --- /dev/null +++ b/backend/src/utils/convert.js @@ -0,0 +1,11 @@ +const marked = require("marked"); + +async function mdToHtml(md_content) { + if (md_content) { + return marked.parse(md_content); + } else { + return ""; + } +} + +module.exports = { mdToHtml }; \ No newline at end of file diff --git a/backend/src/utils/crypto.js b/backend/src/utils/crypto.js new file mode 100644 index 0000000..be1ffc5 --- /dev/null +++ b/backend/src/utils/crypto.js @@ -0,0 +1,61 @@ +// --- Imports --- +const jwt = require("jsonwebtoken"); +const bcrypt = require("bcrypt"); +const { getConfig, getJWTSecret } = require("./configManager"); + + +// --- Config --- + +// Declarations +let JWT_Secret; +let token_expiry; +// Constant values +const saltRounds = 12; +// Load +(async () => { + const config = await getConfig(); + JWT_Secret = await getJWTSecret(); + token_expiry = config.auth.tokenExpiry; + signature_algorithm = config.auth.signatureAlgorithm; +})(); + + +// --- Functions --- + +async function hashPassword(passwd) { + const hash = bcrypt.hashSync(passwd, saltRounds); + return hash; +} + + +async function passwordsMatch(password, hashed_password) { + return await bcrypt.compare(password, hashed_password); +} + + +async function signToken(payload, options = null) { + if (options == null) { + return jwt.sign(payload, JWT_Secret, { expiresIn: token_expiry, }); + } + else { + return jwt.sign(payload, JWT_Secret, options); + } + +} + + +function verifyToken(token) { + return new Promise( async (resolve, reject) => { + await jwt.verify( token, JWT_Secret, (err, user) => { + if (err) { + reject(err); + } else { + resolve(user); + } + }); + }); +} + + +// --- Exports --- +module.exports = { passwordsMatch, hashPassword, verifyToken, signToken }; \ No newline at end of file diff --git a/backend/src/utils/sanitize.js b/backend/src/utils/sanitize.js new file mode 100644 index 0000000..be3885b --- /dev/null +++ b/backend/src/utils/sanitize.js @@ -0,0 +1,19 @@ +const createDOMPurify = require("dompurify"); +const { JSDOM } = require("jsdom"); + +// Initialize +const window = new JSDOM("").window; +const DOMPurify = createDOMPurify(window); + +async function sanitizeText(text) { + return DOMPurify.sanitize(text); +} + +async function sanitizeModData(mod_data) { + console.warn("Skipping sanitanization (not implemented)"); + // mod_data.displayName = await sanitizeText(mod_data.displayName); + // mod_data.otherInfos.description = await sanitizeText(mod_data.otherInfos.description); + // mod_data.otherInfos.changelogs = await sanitizeText(mod_data.otherInfos.changelogs); +} + +module.exports = { sanitizeText, sanitizeModData }; \ No newline at end of file diff --git a/backend/src/utils/validate.js b/backend/src/utils/validate.js new file mode 100644 index 0000000..b7c3519 --- /dev/null +++ b/backend/src/utils/validate.js @@ -0,0 +1,51 @@ +// --- Imports --- +const AppError = require("./appError"); + + +// --- Functions --- + +async function validateNewModData(mod_data) { + + throw new AppError(501, "Not implemented"); + //TODO + // try { + // node_schemas.validateNewModData(node_data); + // } catch (err) { + // throw new AppError(400, "Missing or invalid fields", "Bad request", err); + // } + +} + + +async function validateNewUserData(user_data) { + + throw new AppError(501, "Not implemented"); + //TODO + // try { + // node_schemas.validateNewUserData(node_data); + // } catch (err) { + // throw new AppError(400, "Missing or invalid fields", "Bad request", err); + // } + +} + +async function validateCretendials(identifier, password) { + + throw new AppError(501, "Not implemented"); +} + + +// --- Utils --- + +async function isEmail(text) { + const email_regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; + return email_regex.test(text); +} + +async function isID(text) { + const id_regex = /[a-zA-Z0-9_]+/; + return id_regex.test(text); +} + + +module.exports = { validateNewModData, validateNewUserData, isEmail, isID }; \ No newline at end of file diff --git a/backend/src/utils/validate_legacy.js b/backend/src/utils/validate_legacy.js new file mode 100644 index 0000000..63c605b --- /dev/null +++ b/backend/src/utils/validate_legacy.js @@ -0,0 +1,98 @@ +const mod_model = require("../models/mod"); +const user_model = require("../models/user"); +const AppError = require("./appError"); + +async function validateModData(mod_data) { + //TODO WIP + // Check fields existence + const not_null = mod_data && + Object.keys(mod_data).length == 5 && + mod_data.name && + mod_data.displayName && + mod_data.author && + mod_data.versions != null; + + // mod_data.otherInfos != null && + // Object.keys(mod_data.otherInfos).length == 0 && + // mod_data.otherInfos.description != null && + // mod_data.otherInfos.links != null && + // mod_data.otherInfos.tags != null && + // mod_data.otherInfos.screenshots != null && + // mod_data.otherInfos.license != null && + // mod_data.otherInfos.changelogs != null; + + if (!not_null) { + console.debug("Item is missing expected fields:", mod_data); + throw new AppError(400, "Bad request", "Missing expected fields"); + } + + // Check fields format (check if sanitized) + const is_valid_name = /^[a-zA-Z0-9_]+$/.test(mod_data.name); + const is_valid_displayName = true; + // const is_valid_displayName = /^[a-zA-Z0-9_]+$/.test(mod_data.name); // Temporary + // const + + const is_valid = is_valid_name && is_valid_displayName; + if (!is_valid) { + console.debug("Fields are not following the expected formats"); + throw new AppError(400, "Bad request", "The provided fields don't match the expected format"); + } + + // Check if mod already exists + const exists = await mod_model.exists(mod_data.name); + if (exists) { + console.debug("Error: Item already exists"); + throw new AppError(403, "Forbidden", "Content with this name already exists"); + } +} + + +async function validateUserData(user_data) { + throw new AppError(501, "Not implemented"); + + //TODO + + // Check fields existence + // ... + + if (!not_null) { + console.debug("Missing expected fields:", mod_data); + throw new AppError(400, "Bad request: Missing expected fields"); + } + + // Check fields format (check if sanitized) + const is_valid_username = /^[a-zA-Z0-9_]+$/.test(user_data.username); + // const is_valid_email = ... + // ... + + const is_valid = is_valid_username && is_valid_email; + if (!is_valid) { + console.debug("Fields are not following the expected formats"); + throw new AppError(400, "Bad request: The provided fields don't match the expected format"); + } + + // Check if user already exists + const exists = await user_model.exists(user_data.username); + if (exists) { + console.debug("Error: User already exists"); + throw new AppError(403, "Forbidden: User with this name already exists"); + } +} + +async function validateCretendials(identifier, password) { + + throw new AppError(501, "Not implemented"); +} + +async function isEmail(text) { + const email_regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; + return email_regex.test(text); +} + +async function isID(text) { + const id_regex = /[a-zA-Z0-9_]+/; + return id_regex.test(text); +} + + +module.exports = { validateModData, validateUserData, isEmail, isID }; \ No newline at end of file