diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2cfd95d..a9b4189 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,8 @@ "Bash(node -e:*)", "Bash(npx convex dev:*)", "Bash(npx convex:*)", - "Bash(npx @convex-dev/auth:*)" + "Bash(npx @convex-dev/auth:*)", + "Bash(dir:*)" ] } } diff --git a/.gitignore b/.gitignore index 97cb28f..5241046 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules .env +.env.local .vscode -./backend/uploads \ No newline at end of file +./backend/uploads/ diff --git a/Backend/data/gif_categories.json b/Backend/data/gif_categories.json deleted file mode 100644 index cf5a38f..0000000 --- a/Backend/data/gif_categories.json +++ /dev/null @@ -1,240 +0,0 @@ -{ - "categories": [ - { - "name": "excited", - "src": "https://media.tenor.com/nb9yRj8rHo4AAAPs/excited-ah.webm" - }, - { - "name": "bye", - "src": "https://media.tenor.com/63TUZpfzGHQAAAPs/peach-and-goma.webm" - }, - { - "name": "sorry", - "src": "https://media.tenor.com/MXhgLF7-FwAAAAPs/sorry.webm" - }, - { - "name": "congratulations", - "src": "https://media.tenor.com/Twpbh1jgXD4AAAPs/congratulations-congrats.webm" - }, - { - "name": "sleepy", - "src": "https://media.tenor.com/Clvc7JcH7z4AAAPs/sleepy-bed-time.webm" - }, - { - "name": "hello", - "src": "https://media.tenor.com/Y6Qq56KBIjUAAAPs/hello-cute.webm" - }, - { - "name": "hugs", - "src": "https://media.tenor.com/oZtU0xcJCdMAAAPs/i-love-you-love-you.webm" - }, - { - "name": "ok", - "src": "https://media.tenor.com/UrIakXGExfUAAAPs/mr-bean.webm" - }, - { - "name": "please", - "src": "https://media.tenor.com/heEyHbV8iaUAAAPs/puss-in-boots-shrek.webm" - }, - { - "name": "thank you", - "src": "https://media.tenor.com/L30xqxi-6L4AAAPs/gitapro3-gitagita.webm" - }, - { - "name": "miss you", - "src": "https://media.tenor.com/rzG9YBjxW-0AAAPs/peach-sad.webm" - }, - { - "name": "wink", - "src": "https://media.tenor.com/KfRIf22QpTQAAAPs/wink-eye-wink.webm" - }, - { - "name": "whatever", - "src": "https://media.tenor.com/5AdGWfejMcoAAAPs/whatever-you-say.webm" - }, - { - "name": "hungry", - "src": "https://media.tenor.com/7tTScpb6gt0AAAPs/spearhyunho.webm" - }, - { - "name": "dance", - "src": "https://media.tenor.com/HCyNMWQv868AAAPs/good-night.webm" - }, - { - "name": "annoyed", - "src": "https://media.tenor.com/6DRQNAOEavcAAAPs/cat-annoyed.webm" - }, - { - "name": "omg", - "src": "https://media.tenor.com/FtUKdJxBRH0AAAPs/cat-cat-memes.webm" - }, - { - "name": "crazy", - "src": "https://media.tenor.com/o2yRyjihS1wAAAPs/balthazar-crazy.webm" - }, - { - "name": "shrug", - "src": "https://media.tenor.com/Zc-ZTPzlEHoAAAPs/i-don%27t-know-idk.webm" - }, - { - "name": "smile", - "src": "https://media.tenor.com/NrEEi7ZDo8cAAAPs/as.webm" - }, - { - "name": "awkward", - "src": "https://media.tenor.com/6Ug_C4RjC4kAAAPs/%D9%84%D9%8A%D9%8A%D9%88%D9%88.webm" - }, - { - "name": "ew", - "src": "https://media.tenor.com/oj4p9mRXeoIAAAPs/dol-huh.webm" - }, - { - "name": "angry", - "src": "https://media.tenor.com/RZzU2_IbHDEAAAPs/cat-side-eye.webm" - }, - { - "name": "surprised", - "src": "https://media.tenor.com/CNI1fSM1XSoAAAPs/shocked-surprised.webm" - }, - { - "name": "why", - "src": "https://media.tenor.com/HewFmNRxtT4AAAPs/why-persian-room-cat-guardian.webm" - }, - { - "name": "thumbs up", - "src": "https://media.tenor.com/LpEzkHFtdgUAAAPs/gif-emoji.webm" - }, - { - "name": "wow", - "src": "https://media.tenor.com/VdsC5bF7CMAAAAPs/smu.webm" - }, - { - "name": "ouch", - "src": "https://media.tenor.com/qG2Qj0vvLwMAAAPs/ouch.webm" - }, - { - "name": "oops", - "src": "https://media.tenor.com/izYQUXHzhxoAAAPs/oops.webm" - }, - { - "name": "youre welcome", - "src": "https://media.tenor.com/CXZLJK_6sa0AAAPs/you%27re-welcome.webm" - }, - { - "name": "lazy", - "src": "https://media.tenor.com/Xt7ns1EZUEIAAAPs/panda-animated.webm" - }, - { - "name": "stressed", - "src": "https://media.tenor.com/Yc-62-d3QCAAAAPs/stressed.webm" - }, - { - "name": "embarrassed", - "src": "https://media.tenor.com/EIlaqgHLePUAAAPs/look-away-simpson.webm" - }, - { - "name": "clapping", - "src": "https://media.tenor.com/3yPBPC_dwe8AAAPs/leonardo-dicaprio-clapping.webm" - }, - { - "name": "awesome", - "src": "https://media.tenor.com/WFhElAbsdqcAAAPs/awesome-minions.webm" - }, - { - "name": "jk", - "src": "https://media.tenor.com/Fy0hkZaMgakAAAPs/nah-im-just-kidding-jk.webm" - }, - { - "name": "good luck", - "src": "https://media.tenor.com/VK6Wv4nxX10AAAPs/good-luck.webm" - }, - { - "name": "high five", - "src": "https://media.tenor.com/HozyHCAac-kAAAPs/high-five-patrick-star.webm" - }, - { - "name": "nervous", - "src": "https://media.tenor.com/tDfXlJnVctgAAAPs/sweating-nervous.webm" - }, - { - "name": "duh", - "src": "https://media.tenor.com/DCScm2moJ7EAAAPs/duh-sarcastic.webm" - }, - { - "name": "aww", - "src": "https://media.tenor.com/XwxrDV7VKuEAAAPs/love-languages.webm" - }, - { - "name": "scared", - "src": "https://media.tenor.com/Io0g8LOf8nMAAAPs/dog-awkward.webm" - }, - { - "name": "bored", - "src": "https://media.tenor.com/f4d9ExQT1voAAAPs/h2di-cat-dead.webm" - }, - { - "name": "sigh", - "src": "https://media.tenor.com/ZFc20z8DItkAAAPs/facepalm-really.webm" - }, - { - "name": "kiss", - "src": "https://media.tenor.com/RPq56gVYswUAAAPs/twitter-kiwi.webm" - }, - { - "name": "sad", - "src": "https://media.tenor.com/lV1EF4I83MkAAAPs/bubu-dudu-twitter.webm" - }, - { - "name": "good night", - "src": "https://media.tenor.com/95lulIY57IwAAAPs/sweet-dreams-sleep-good.webm" - }, - { - "name": "good morning", - "src": "https://media.tenor.com/25Y5C_HeRckAAAPs/good-morning.webm" - }, - { - "name": "confused", - "src": "https://media.tenor.com/ASAsHQNVvacAAAPs/midnight-the-cat-stopped-working-midnight-the-cat.webm" - }, - { - "name": "chill out", - "src": "https://media.tenor.com/65BBEN4WDcUAAAPs/chillin-chilling.webm" - }, - { - "name": "love", - "src": "https://media.tenor.com/1nIDXbABxgsAAAPs/gif-gifkk.webm" - }, - { - "name": "happy", - "src": "https://media.tenor.com/gotOLnyvy4YAAAPs/bubu-dancing-dance.webm" - }, - { - "name": "cry", - "src": "https://media.tenor.com/rokauCI9nPYAAAPs/crying-sad.webm" - }, - { - "name": "yes", - "src": "https://media.tenor.com/7iq8qyXvKHsAAAPs/yes-monkey.webm" - }, - { - "name": "no", - "src": "https://media.tenor.com/vLK4Mq3jiKIAAAPs/cat-no.webm" - }, - { - "name": "lol", - "src": "https://media.tenor.com/JgCg0MPmMWQAAAPs/shirley-temple-lol.webm" - } - ], - "gifs": [ - { - "id": "13011029513751011075", - "title": "", - "url": "https://tenor.com/view/feliz-dia-de-reyes-happy-three-kings-day-epiphany-3kings-day-magos-or-el-d%C3%ADa-de-reyes-gif-19847132", - "src": "https://media.tenor.com/tJB2aElAnwMAAAPs/feliz-dia-de-reyes-happy-three-kings-day.webm", - "gif_src": "https://media.tenor.com/tJB2aElAnwMAAAAC/feliz-dia-de-reyes-happy-three-kings-day.gif", - "width": 640, - "height": 640, - "preview": "https://media.tenor.com/tJB2aElAnwMAAAAD/feliz-dia-de-reyes-happy-three-kings-day.png" - } - ] -} diff --git a/Backend/db.js b/Backend/db.js deleted file mode 100644 index d2cbf69..0000000 --- a/Backend/db.js +++ /dev/null @@ -1,10 +0,0 @@ -const { Pool } = require('pg'); -require('dotenv').config(); - -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, -}); - -module.exports = { - query: (text, params) => pool.query(text, params), -}; diff --git a/Backend/package-lock.json b/Backend/package-lock.json deleted file mode 100644 index b1f3735..0000000 --- a/Backend/package-lock.json +++ /dev/null @@ -1,1735 +0,0 @@ -{ - "name": "backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "backend", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "axios": "^1.13.2", - "cors": "^2.8.5", - "dotenv": "^17.2.3", - "express": "^5.2.1", - "livekit-server-sdk": "^2.15.0", - "multer": "^2.0.2", - "pg": "^8.16.3", - "redis": "^5.10.0", - "socket.io": "^4.8.3" - } - }, - "node_modules/@bufbuild/protobuf": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", - "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)" - }, - "node_modules/@livekit/protocol": { - "version": "1.43.4", - "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.43.4.tgz", - "integrity": "sha512-mJDFt/p+G2OKmIGizYiACK7Jb06wd42m9Pe7Y9cAOfdYpvwCqHlw4yul5Z7iRU3VKPsYJ27WL3oeHEoiu+HuAA==", - "license": "Apache-2.0", - "dependencies": { - "@bufbuild/protobuf": "^1.10.0" - } - }, - "node_modules/@redis/bloom": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.10.0.tgz", - "integrity": "sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.10.0" - } - }, - "node_modules/@redis/client": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.10.0.tgz", - "integrity": "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA==", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@redis/json": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.10.0.tgz", - "integrity": "sha512-B2G8XlOmTPUuZtD44EMGbtoepQG34RCDXLZbjrtON1Djet0t5Ri7/YPXvL9aomXqP8lLTreaprtyLKF4tmXEEA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.10.0" - } - }, - "node_modules/@redis/search": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.10.0.tgz", - "integrity": "sha512-3SVcPswoSfp2HnmWbAGUzlbUPn7fOohVu2weUQ0S+EMiQi8jwjL+aN2p6V3TI65eNfVsJ8vyPvqWklm6H6esmg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.10.0" - } - }, - "node_modules/@redis/time-series": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.10.0.tgz", - "integrity": "sha512-cPkpddXH5kc/SdRhF0YG0qtjL+noqFT0AcHbQ6axhsPsO7iqPi1cjxgdkE9TNeKiBUUdCaU1DbqkR/LzbzPBhg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.10.0" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", - "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-9.1.3.tgz", - "integrity": "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==", - "license": "MIT", - "dependencies": { - "camelcase": "^8.0.0", - "map-obj": "5.0.0", - "quick-lru": "^6.1.1", - "type-fest": "^4.3.2" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/engine.io": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", - "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", - "license": "MIT", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.18.3" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.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.0", - "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.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", - "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/livekit-server-sdk": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/livekit-server-sdk/-/livekit-server-sdk-2.15.0.tgz", - "integrity": "sha512-HmzjWnwEwwShu8yUf7VGFXdc+BuMJR5pnIY4qsdlhqI9d9wDgq+4cdTEHg0NEBaiGnc6PCOBiaTYgmIyVJ0S9w==", - "license": "Apache-2.0", - "dependencies": { - "@bufbuild/protobuf": "^1.10.1", - "@livekit/protocol": "^1.43.1", - "camelcase-keys": "^9.0.0", - "jose": "^5.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/map-obj": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.0.tgz", - "integrity": "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", - "license": "MIT", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" - }, - "engines": { - "node": ">= 10.16.0" - } - }, - "node_modules/multer/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.2.7" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quick-lru": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", - "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/redis": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/redis/-/redis-5.10.0.tgz", - "integrity": "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw==", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.10.0", - "@redis/client": "5.10.0", - "@redis/json": "5.10.0", - "@redis/search": "5.10.0", - "@redis/time-series": "5.10.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/socket.io": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", - "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", - "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", - "license": "MIT", - "dependencies": { - "debug": "~4.4.1", - "ws": "~8.18.3" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", - "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "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 - } - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - } - } -} diff --git a/Backend/package.json b/Backend/package.json deleted file mode 100644 index 3f35210..0000000 --- a/Backend/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "backend", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "axios": "^1.13.2", - "cors": "^2.8.5", - "dotenv": "^17.2.3", - "express": "^5.2.1", - "livekit-server-sdk": "^2.15.0", - "multer": "^2.0.2", - "pg": "^8.16.3", - "redis": "^5.10.0", - "socket.io": "^4.8.3" - } -} diff --git a/Backend/redis.js b/Backend/redis.js deleted file mode 100644 index 0152080..0000000 --- a/Backend/redis.js +++ /dev/null @@ -1,15 +0,0 @@ -const { createClient } = require('redis'); -require('dotenv').config(); - -const client = createClient({ - url: process.env.REDIS_URL -}); - -client.on('error', (err) => console.log('Redis Client Error', err)); - -(async () => { - await client.connect(); - console.log('Redis connected'); -})(); - -module.exports = client; diff --git a/Backend/routes/auth.js b/Backend/routes/auth.js deleted file mode 100644 index 694e496..0000000 --- a/Backend/routes/auth.js +++ /dev/null @@ -1,158 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const db = require('../db'); -const crypto = require('crypto'); - -// Helper to generate fake salt for user privacy -function generateFakeSalt(username) { - return crypto.createHmac('sha256', 'SERVER_SECRET_KEY') // In prod, use env var - .update(username) - .digest('hex'); -} - -router.post('/register', async (req, res) => { - const { username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys, inviteCode } = req.body; - - try { - // Step 1: Enforce Invite (unless first user) - const userCountRes = await db.query('SELECT count(*) FROM users'); - const userCount = parseInt(userCountRes.rows[0].count); - - if (userCount > 0) { - if (!inviteCode) { - return res.status(403).json({ error: 'Invite code required' }); - } - - // Check Invite validity - const inviteRes = await db.query('SELECT * FROM invites WHERE code = $1', [inviteCode]); - if (inviteRes.rows.length === 0) { - return res.status(403).json({ error: 'Invalid invite code' }); - } - - var invite = inviteRes.rows[0]; - - // Check Expiration - if (invite.expires_at && new Date() > new Date(invite.expires_at)) { - return res.status(410).json({ error: 'Invite expired' }); - } - - // Check Usage Limits - if (invite.max_uses !== null && invite.uses >= invite.max_uses) { - return res.status(410).json({ error: 'Invite max uses reached' }); - } - } - - // START TRANSACTION - To ensure invite usage and user creation are atomic - await db.query('BEGIN'); - - try { - // Update Invite Usage (only if enforced) - if (userCount > 0) { - await db.query('UPDATE invites SET uses = uses + 1 WHERE code = $1', [inviteCode]); - } - - // Create User - // Create User - const result = await db.query( - `INSERT INTO users (username, client_salt, encrypted_master_key, hashed_auth_key, public_identity_key, public_signing_key, encrypted_private_keys) - VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, - [username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys] - ); - const newUserId = result.rows[0].id; - - // Assign Roles - // 1. @everyone (Always) - await db.query(` - INSERT INTO user_roles (user_id, role_id) - SELECT $1, id FROM roles WHERE name = '@everyone' - `, [newUserId]); - - // 2. Owner (If first user or if admin logic allows) - if (userCount === 0) { - await db.query(` - INSERT INTO user_roles (user_id, role_id) - SELECT $1, id FROM roles WHERE name = 'Owner' - `, [newUserId]); - - // Also set is_admin = true for legacy support - await db.query('UPDATE users SET is_admin = TRUE WHERE id = $1', [newUserId]); - } - - await db.query('COMMIT'); - res.json({ success: true, userId: result.rows[0].id }); - - } catch (txErr) { - await db.query('ROLLBACK'); - throw txErr; - } - - } catch (err) { - console.error(err); - if (err.code === '23505') { // Unique violation - res.status(400).json({ error: 'Username taken' }); - } else { - res.status(500).json({ error: 'Server error' }); - } - } -}); - -router.post('/login/salt', async (req, res) => { - const { username } = req.body; - try { - const result = await db.query('SELECT client_salt FROM users WHERE username = $1', [username]); - if (result.rows.length > 0) { - res.json({ salt: result.rows[0].client_salt }); - } else { - // Return fake salt to prevent enumeration - res.json({ salt: generateFakeSalt(username) }); - } - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -router.post('/login/verify', async (req, res) => { - const { username, dak } = req.body; - - try { - const result = await db.query( - 'SELECT id, hashed_auth_key, encrypted_master_key, encrypted_private_keys, public_identity_key FROM users WHERE username = $1', - [username] - ); - - if (result.rows.length === 0) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - const user = result.rows[0]; - const hashedDAK = crypto.createHash('sha256').update(dak).digest('hex'); - - if (hashedDAK === user.hashed_auth_key) { - res.json({ - success: true, - userId: user.id, - encryptedMK: user.encrypted_master_key, - encryptedPrivateKeys: user.encrypted_private_keys, - publicKey: user.public_identity_key // Return Public Key - }); - } else { - res.status(401).json({ error: 'Invalid credentials' }); - } - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -router.get('/users/public-keys', async (req, res) => { - try { - const result = await db.query('SELECT id, username, public_identity_key FROM users'); - res.json(result.rows); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -module.exports = router; diff --git a/Backend/routes/channels.js b/Backend/routes/channels.js deleted file mode 100644 index 4c28b2c..0000000 --- a/Backend/routes/channels.js +++ /dev/null @@ -1,160 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const db = require('../db'); - -router.get('/', async (req, res) => { - try { - const result = await db.query("SELECT * FROM channels WHERE type != 'dm' ORDER BY name ASC"); - res.json(result.rows); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Create New Channel -router.post('/create', async (req, res) => { - console.log('Creates Channel Body:', req.body); - const { name, type } = req.body; - if (!name) return res.status(400).json({ error: 'Channel name required' }); - - try { - const result = await db.query( - 'INSERT INTO channels (name, type) VALUES ($1, $2) RETURNING *', - [name, type || 'text'] - ); - const newChannel = result.rows[0]; - // DO NOT emit 'new_channel' here. Wait until keys are uploaded. - res.json({ id: newChannel.id }); - } catch (err) { - console.error('Error creating channel:', err); - if (err.code === '23505') { - res.status(400).json({ error: 'Channel already exists' }); - } else { - res.status(500).json({ error: 'Server error' }); - } - } -}); - -// Notify Channel Creation (Called AFTER keys are uploaded) -router.post('/:id/notify', async (req, res) => { - const { id } = req.params; - try { - const result = await db.query('SELECT * FROM channels WHERE id = $1', [id]); - if (result.rows.length === 0) return res.status(404).json({ error: 'Channel not found' }); - - const channel = result.rows[0]; - if (req.io) req.io.emit('new_channel', channel); // Emit NOW - - res.json({ success: true }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Upload Channel Keys (for self or others) -// Upload Channel Keys (for self or others) - Supports Batch -router.post('/keys', async (req, res) => { - // Check if body is array - const keysToUpload = Array.isArray(req.body) ? req.body : [req.body]; - - if (keysToUpload.length === 0) return res.json({ success: true }); - - try { - await db.query('BEGIN'); - - for (const keyData of keysToUpload) { - const { channelId, userId, encryptedKeyBundle, keyVersion } = keyData; - - if (!channelId || !userId || !encryptedKeyBundle) { - continue; - } - - await db.query( - `INSERT INTO channel_keys (channel_id, user_id, encrypted_key_bundle, key_version) - VALUES ($1, $2, $3, $4) - ON CONFLICT (channel_id, user_id) DO UPDATE - SET encrypted_key_bundle = EXCLUDED.encrypted_key_bundle, - key_version = EXCLUDED.key_version`, - [channelId, userId, encryptedKeyBundle, keyVersion || 1] - ); - } - - await db.query('COMMIT'); - res.json({ success: true, count: keysToUpload.length }); - } catch (err) { - await db.query('ROLLBACK'); - console.error('Error uploading channel keys:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Get User's Channel Keys -router.get('/keys/:userId', async (req, res) => { - const { userId } = req.params; - try { - const result = await db.query( - 'SELECT channel_id, encrypted_key_bundle, key_version FROM channel_keys WHERE user_id = $1', - [userId] - ); - res.json(result.rows); - } catch (err) { - console.error('Error fetching channel keys:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Update Channel Name -router.put('/:id', async (req, res) => { - const { id } = req.params; - const { name } = req.body; - - if (!name) return res.status(400).json({ error: 'Name required' }); - - try { - const result = await db.query( - 'UPDATE channels SET name = $1 WHERE id = $2 RETURNING *', - [name, id] - ); - if (result.rows.length === 0) return res.status(404).json({ error: 'Channel not found' }); - - const updatedChannel = result.rows[0]; - if (req.io) req.io.emit('channel_renamed', updatedChannel); - res.json(updatedChannel); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Delete Channel -router.delete('/:id', async (req, res) => { - const { id } = req.params; - - try { - await db.query('BEGIN'); - - // Manual Cascade (since we didn't set FK CASCADE in schema for these yet) - await db.query('DELETE FROM messages WHERE channel_id = $1', [id]); - await db.query('DELETE FROM channel_keys WHERE channel_id = $1', [id]); - - const result = await db.query('DELETE FROM channels WHERE id = $1 RETURNING *', [id]); - - if (result.rows.length === 0) { - await db.query('ROLLBACK'); - return res.status(404).json({ error: 'Channel not found' }); - } - - await db.query('COMMIT'); - - if (req.io) req.io.emit('channel_deleted', id); - res.json({ success: true, deletedId: id }); - } catch (err) { - await db.query('ROLLBACK'); - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -module.exports = router; diff --git a/Backend/routes/dms.js b/Backend/routes/dms.js deleted file mode 100644 index f849723..0000000 --- a/Backend/routes/dms.js +++ /dev/null @@ -1,79 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const db = require('../db'); - -// POST /api/dms/open — Find-or-create a DM channel between two users -router.post('/open', async (req, res) => { - const { userId, targetUserId } = req.body; - - if (!userId || !targetUserId) { - return res.status(400).json({ error: 'userId and targetUserId required' }); - } - - if (userId === targetUserId) { - return res.status(400).json({ error: 'Cannot DM yourself' }); - } - - // Deterministic channel name so the same pair always maps to one channel - const sorted = [userId, targetUserId].sort(); - const dmName = `dm-${sorted[0]}-${sorted[1]}`; - - try { - // Check if DM channel already exists - const existing = await db.query( - 'SELECT id FROM channels WHERE name = $1 AND type = $2', - [dmName, 'dm'] - ); - - if (existing.rows.length > 0) { - return res.json({ channelId: existing.rows[0].id, created: false }); - } - - // Create the DM channel + participants in a transaction - await db.query('BEGIN'); - - const channelResult = await db.query( - 'INSERT INTO channels (name, type) VALUES ($1, $2) RETURNING id', - [dmName, 'dm'] - ); - const channelId = channelResult.rows[0].id; - - await db.query( - 'INSERT INTO dm_participants (channel_id, user_id) VALUES ($1, $2), ($1, $3)', - [channelId, userId, targetUserId] - ); - - await db.query('COMMIT'); - - res.json({ channelId, created: true }); - } catch (err) { - await db.query('ROLLBACK'); - console.error('Error opening DM:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// GET /api/dms/user/:userId — List all DM channels for a user with the other user's info -router.get('/user/:userId', async (req, res) => { - const { userId } = req.params; - - try { - const result = await db.query(` - SELECT c.id AS channel_id, c.name AS channel_name, c.created_at, - other_user.id AS other_user_id, other_user.username AS other_username - FROM dm_participants my - JOIN channels c ON c.id = my.channel_id AND c.type = 'dm' - JOIN dm_participants other ON other.channel_id = my.channel_id AND other.user_id != $1 - JOIN users other_user ON other_user.id = other.user_id - WHERE my.user_id = $1 - ORDER BY c.created_at DESC - `, [userId]); - - res.json(result.rows); - } catch (err) { - console.error('Error fetching DM channels:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -module.exports = router; diff --git a/Backend/routes/invites.js b/Backend/routes/invites.js deleted file mode 100644 index a468b32..0000000 --- a/Backend/routes/invites.js +++ /dev/null @@ -1,75 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const db = require('../db'); - -// Create a new invite -router.post('/create', async (req, res) => { - const { code, encryptedPayload, createdBy, maxUses, expiresAt, keyVersion } = req.body; - - if (!code || !encryptedPayload || !createdBy || !keyVersion) { - return res.status(400).json({ error: 'Missing required fields' }); - } - - try { - await db.query( - `INSERT INTO invites (code, encrypted_payload, created_by, max_uses, expires_at, key_version) - VALUES ($1, $2, $3, $4, $5, $6)`, - [code, encryptedPayload, createdBy, maxUses || null, expiresAt || null, keyVersion] - ); - res.json({ success: true }); - } catch (err) { - console.error('Error creating invite:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Fetch an invite (and validate it) -router.get('/:code', async (req, res) => { - const { code } = req.params; - - try { - const result = await db.query('SELECT * FROM invites WHERE code = $1', [code]); - if (result.rows.length === 0) { - return res.status(404).json({ error: 'Invite not found' }); - } - - const invite = result.rows[0]; - - // Check Expiration - if (invite.expires_at && new Date() > new Date(invite.expires_at)) { - return res.status(410).json({ error: 'Invite expired' }); - } - - // Check Usage Limits - if (invite.max_uses !== null && invite.uses >= invite.max_uses) { - return res.status(410).json({ error: 'Invite max uses reached' }); - } - - // Increment Uses - await db.query('UPDATE invites SET uses = uses + 1 WHERE code = $1', [code]); - - res.json({ - encryptedPayload: invite.encrypted_payload, - keyVersion: invite.key_version - }); - - } catch (err) { - console.error('Error fetching invite:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Delete an invite (Revoke) -> Triggers client-side key rotation policy warning? -// The client should call this, then rotate keys. -router.delete('/:code', async (req, res) => { - const { code } = req.params; - try { - await db.query('DELETE FROM invites WHERE code = $1', [code]); - res.json({ success: true }); - } catch (err) { - console.error('Error deleting invite:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -module.exports = router; diff --git a/Backend/routes/roles.js b/Backend/routes/roles.js deleted file mode 100644 index 009f3d6..0000000 --- a/Backend/routes/roles.js +++ /dev/null @@ -1,213 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const db = require('../db'); - -// Middleware to check for permissions (simplified for now) -// In a real app, you'd check if req.user has the permission -// Middleware to check for permissions -const checkPermission = (requiredPerm) => { - return async (req, res, next) => { - const userId = req.headers['x-user-id']; - - if (!userId) { - return res.status(401).json({ error: 'Unauthorized: No User ID' }); - } - - try { - // Fetch all roles for user and aggregate permissions - const result = await db.query(` - SELECT r.permissions - FROM user_roles ur - JOIN roles r ON ur.role_id = r.id - WHERE ur.user_id = $1 - `, [userId]); - - let hasPermission = false; - - // Check if ANY of the user's roles has the permission - for (const row of result.rows) { - const perms = row.permissions || {}; - // If Manage Roles is true, or if checking for something else and they have it - if (perms[requiredPerm] === true) { - hasPermission = true; - break; - } - // Implicit Admin/Owner check: if they have 'manage_roles' and we are checking something lower? - // For "Owner" role, we seeded it with specific true values. - // But let's check for 'administrator' equivalent if we had it. - // For now, implicit check: if we need 'manage_roles', look for it. - } - - if (!hasPermission) { - return res.status(403).json({ error: `Forbidden: Missing ${requiredPerm}` }); - } - - next(); - } catch (err) { - console.error('Permission check failed:', err); - return res.status(500).json({ error: 'Internal Server Error' }); - } - }; -}; - -// GET /api/roles/permissions - Get current user's permissions -router.get('/permissions', async (req, res) => { - const userId = req.headers['x-user-id']; - if (!userId) return res.json({}); // No perms if no ID - - try { - const result = await db.query(` - SELECT r.permissions - FROM user_roles ur - JOIN roles r ON ur.role_id = r.id - WHERE ur.user_id = $1 - `, [userId]); - - const finalPerms = { - manage_channels: false, - manage_roles: false, - create_invite: false, - embed_links: false, - attach_files: false - }; - - for (const row of result.rows) { - const p = row.permissions || {}; - if (p.manage_channels) finalPerms.manage_channels = true; - if (p.manage_roles) finalPerms.manage_roles = true; - if (p.create_invite) finalPerms.create_invite = true; - if (p.embed_links) finalPerms.embed_links = true; - if (p.attach_files) finalPerms.attach_files = true; - } - res.json(finalPerms); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// GET /api/roles - List all roles -router.get('/', async (req, res) => { - try { - const result = await db.query('SELECT * FROM roles ORDER BY position DESC, id ASC'); - res.json(result.rows); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// POST /api/roles - Create new role -router.post('/', checkPermission('manage_roles'), async (req, res) => { - const { name, color, permissions, position, is_hoist } = req.body; - try { - const result = await db.query( - 'INSERT INTO roles (name, color, permissions, position, is_hoist) VALUES ($1, $2, $3, $4, $5) RETURNING *', - [name || 'new role', color || '#99aab5', permissions || {}, position || 0, is_hoist || false] - ); - res.json(result.rows[0]); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// PUT /api/roles/:id - Update role -router.put('/:id', checkPermission('manage_roles'), async (req, res) => { - const { id } = req.params; - const { name, color, permissions, position, is_hoist } = req.body; - - // Dynamic update query - const fields = []; - const values = []; - let idx = 1; - - if (name !== undefined) { fields.push(`name = $${idx++}`); values.push(name); } - if (color !== undefined) { fields.push(`color = $${idx++}`); values.push(color); } - if (permissions !== undefined) { fields.push(`permissions = $${idx++}`); values.push(permissions); } - if (position !== undefined) { fields.push(`position = $${idx++}`); values.push(position); } - if (is_hoist !== undefined) { fields.push(`is_hoist = $${idx++}`); values.push(is_hoist); } - - if (fields.length === 0) return res.json({ success: true }); // Nothing to update - - values.push(id); - const query = `UPDATE roles SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`; - - try { - const result = await db.query(query, values); - if (result.rows.length === 0) return res.status(404).json({ error: 'Role not found' }); - res.json(result.rows[0]); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// DELETE /api/roles/:id - Delete role -router.delete('/:id', checkPermission('manage_roles'), async (req, res) => { - const { id } = req.params; - try { - // user_roles cascade handled by DB - const result = await db.query('DELETE FROM roles WHERE id = $1 RETURNING *', [id]); - if (result.rows.length === 0) return res.status(404).json({ error: 'Role not found' }); - res.json({ success: true, deletedId: id }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// GET /api/roles/members - List members with roles -// (This is effectively GET /api/users but enriched with roles) -router.get('/members', async (req, res) => { - try { - const result = await db.query(` - SELECT u.id, u.username, u.public_identity_key, - json_agg(r.*) FILTER (WHERE r.id IS NOT NULL) as roles - FROM users u - LEFT JOIN user_roles ur ON u.id = ur.user_id - LEFT JOIN roles r ON ur.role_id = r.id - GROUP BY u.id - `); - res.json(result.rows); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// POST /api/roles/:id/assign - Assign role to user -router.post('/:id/assign', checkPermission('manage_roles'), async (req, res) => { - const { id } = req.params; // role id - const { userId } = req.body; - - try { - await db.query( - 'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING', - [userId, id] - ); - res.json({ success: true }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -// POST /api/roles/:id/remove - Remove role from user -router.post('/:id/remove', checkPermission('manage_roles'), async (req, res) => { - const { id } = req.params; // role id - const { userId } = req.body; - - try { - await db.query( - 'DELETE FROM user_roles WHERE user_id = $1 AND role_id = $2', - [userId, id] - ); - res.json({ success: true }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Server error' }); - } -}); - -module.exports = router; diff --git a/Backend/routes/upload.js b/Backend/routes/upload.js deleted file mode 100644 index 2d10cee..0000000 --- a/Backend/routes/upload.js +++ /dev/null @@ -1,41 +0,0 @@ -const express = require('express'); -const multer = require('multer'); -const path = require('path'); -const fs = require('fs'); - -const router = express.Router(); - -// Ensure uploads directory exists -const uploadDir = path.join(__dirname, '../uploads'); -if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir); -} - -// Configure Multer Storage -const storage = multer.diskStorage({ - destination: (req, file, cb) => { - cb(null, uploadDir); - }, - filename: (req, file, cb) => { - // Generate unique filename: timestamp-random.ext - const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); - const ext = path.extname(file.originalname); - cb(null, uniqueSuffix + ext); - } -}); - -const upload = multer({ storage: storage }); - -// POST /api/upload -router.post('/', upload.single('file'), (req, res) => { - if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - // Return the URL to access the file - // Assumes server serves 'uploads' folder at '/uploads' - const fileUrl = `/uploads/${req.file.filename}`; - res.json({ url: fileUrl, filename: req.file.filename }); -}); - -module.exports = router; diff --git a/Backend/routes/voice.js b/Backend/routes/voice.js deleted file mode 100644 index 1268c14..0000000 --- a/Backend/routes/voice.js +++ /dev/null @@ -1,64 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const { AccessToken } = require('livekit-server-sdk'); -const db = require('../db'); - -// Middleware to check permissions? -// For now, simpler: assuming user is logged in (via x-user-id header check in frontend) -// Real implementation should use the checkPermission middleware or verify session - -router.post('/token', async (req, res) => { - const { channelId } = req.body; - const userId = req.headers['x-user-id']; // Sent by frontend - - // Default fallback if no user (should rely on auth middleware ideally) - if (!userId) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - try { - // 1. Get Username (for display) - const userRes = await db.query('SELECT username FROM users WHERE id = $1', [userId]); - if (userRes.rows.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - const username = userRes.rows[0].username; - - // 2. Get Channel Name (Optional, for room name check) - // Ensure channel exists and is of type 'voice' - const channelRes = await db.query('SELECT id, type FROM channels WHERE id = $1', [channelId]); - if (channelRes.rows.length === 0) { - return res.status(404).json({ error: 'Channel not found' }); - } - if (channelRes.rows[0].type !== 'voice') { - return res.status(400).json({ error: 'Not a voice channel' }); - } - - // 3. Generate Token - // API Key/Secret from env - const apiKey = process.env.LIVEKIT_API_KEY || 'devkey'; - const apiSecret = process.env.LIVEKIT_API_SECRET || 'secret'; - - const at = new AccessToken(apiKey, apiSecret, { - identity: userId, - name: username, - }); - - at.addGrant({ - roomJoin: true, - room: channelId, - canPublish: true, - canSubscribe: true, - }); - - const token = await at.toJwt(); - - res.json({ token }); - - } catch (err) { - console.error('Error creating voice token:', err); - res.status(500).json({ error: 'Server error' }); - } -}); - -module.exports = router; diff --git a/Backend/schema.sql b/Backend/schema.sql deleted file mode 100644 index 5510fcf..0000000 --- a/Backend/schema.sql +++ /dev/null @@ -1,73 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS "pgcrypto"; - -CREATE TABLE IF NOT EXISTS users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - username TEXT UNIQUE NOT NULL, - client_salt TEXT NOT NULL, - encrypted_master_key TEXT NOT NULL, - hashed_auth_key TEXT NOT NULL, - public_identity_key TEXT NOT NULL, - public_signing_key TEXT NOT NULL, - encrypted_private_keys TEXT NOT NULL, - is_admin BOOLEAN DEFAULT FALSE, - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TABLE IF NOT EXISTS channels ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT UNIQUE NOT NULL, - type TEXT DEFAULT 'text' CHECK (type IN ('text', 'voice', 'dm')), - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TABLE IF NOT EXISTS roles ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - color TEXT DEFAULT '#99aab5', - position INTEGER DEFAULT 0, - permissions JSONB DEFAULT '{}', - is_hoist BOOLEAN DEFAULT FALSE, - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TABLE IF NOT EXISTS user_roles ( - user_id UUID REFERENCES users(id) ON DELETE CASCADE, - role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, role_id) -); - -CREATE TABLE IF NOT EXISTS channel_keys ( - channel_id UUID NOT NULL, - user_id UUID NOT NULL, - encrypted_key_bundle TEXT NOT NULL, - key_version INTEGER DEFAULT 1, - PRIMARY KEY (channel_id, user_id) -); - -CREATE TABLE IF NOT EXISTS messages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL, - sender_id UUID NOT NULL, - ciphertext TEXT NOT NULL, - nonce TEXT NOT NULL, - signature TEXT NOT NULL, - key_version INTEGER NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TABLE IF NOT EXISTS invites ( - code TEXT PRIMARY KEY, -- e.g. "8f1a4c..." (The ID of the invite) - encrypted_payload TEXT NOT NULL, -- The AES-Encrypted Key Bundle (Server can't read this) - created_by UUID REFERENCES users(id) ON DELETE CASCADE, - max_uses INTEGER DEFAULT NULL, -- NULL = Infinite - uses INTEGER DEFAULT 0, - expires_at TIMESTAMPTZ DEFAULT NULL, -- NULL = Never - key_version INTEGER NOT NULL, -- Which key version is inside? (So we can invalidate leaks) - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TABLE IF NOT EXISTS dm_participants ( - channel_id UUID REFERENCES channels(id) ON DELETE CASCADE, - user_id UUID REFERENCES users(id) ON DELETE CASCADE, - PRIMARY KEY (channel_id, user_id) -); diff --git a/Backend/scripts/init-db.js b/Backend/scripts/init-db.js deleted file mode 100644 index 747861a..0000000 --- a/Backend/scripts/init-db.js +++ /dev/null @@ -1,47 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -require('dotenv').config({ path: path.join(__dirname, '../.env') }); -const db = require('../db'); - -async function initDb() { - try { - const schemaPath = path.join(__dirname, '../schema.sql'); - const schemaSql = fs.readFileSync(schemaPath, 'utf8'); - - console.log('Dropping existing tables...'); - await db.query(` - DROP TABLE IF EXISTS invites CASCADE; - DROP TABLE IF EXISTS messages CASCADE; - DROP TABLE IF EXISTS channel_keys CASCADE; - DROP TABLE IF EXISTS roles CASCADE; - DROP TABLE IF EXISTS channels CASCADE; - DROP TABLE IF EXISTS users CASCADE; - `); - - console.log('Applying schema...'); - await db.query(schemaSql); - - console.log('Schema applied successfully.'); - - console.log('Seeding Default Roles...'); - // 1. @everyone (Limited) - await db.query(` - INSERT INTO roles (name, color, position, is_hoist, permissions) - VALUES ('@everyone', '#99aab5', 0, false, '{"manage_channels": false, "manage_roles": false, "create_invite": false, "embed_links": true, "attach_files": true}') - `); - - // 2. Owner (All Permissions) - await db.query(` - INSERT INTO roles (name, color, position, is_hoist, permissions) - VALUES ('Owner', '#ED4245', 100, true, '{"manage_channels": true, "manage_roles": true, "create_invite": true, "embed_links": true, "attach_files": true}') - `); - - console.log('Schema applied successfully.'); - process.exit(0); - } catch (err) { - console.error('Error applying schema:', err); - process.exit(1); - } -} - -initDb(); diff --git a/Backend/scripts/migrate_dms.js b/Backend/scripts/migrate_dms.js deleted file mode 100644 index 875c26d..0000000 --- a/Backend/scripts/migrate_dms.js +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path'); -require('dotenv').config({ path: path.join(__dirname, '../.env') }); -const db = require('../db'); - -async function migrate() { - try { - console.log('Running DM migration...'); - - // 1. Update the check constraint to allow 'dm' type - await db.query("ALTER TABLE channels DROP CONSTRAINT IF EXISTS channels_type_check"); - await db.query("ALTER TABLE channels ADD CONSTRAINT channels_type_check CHECK (type IN ('text', 'voice', 'dm'))"); - console.log(' Updated channels type constraint to allow dm.'); - - // 2. Create dm_participants table - await db.query(` - CREATE TABLE IF NOT EXISTS dm_participants ( - channel_id UUID REFERENCES channels(id) ON DELETE CASCADE, - user_id UUID REFERENCES users(id) ON DELETE CASCADE, - PRIMARY KEY (channel_id, user_id) - ) - `); - console.log(' Created dm_participants table.'); - - console.log('DM migration complete.'); - process.exit(0); - } catch (err) { - console.error('DM migration failed:', err); - process.exit(1); - } -} - -migrate(); diff --git a/Backend/scripts/migrate_voice.js b/Backend/scripts/migrate_voice.js deleted file mode 100644 index 2808bcd..0000000 --- a/Backend/scripts/migrate_voice.js +++ /dev/null @@ -1,17 +0,0 @@ -const path = require('path'); -require('dotenv').config({ path: path.join(__dirname, '../.env') }); -const db = require('../db'); - -async function migrate() { - try { - console.log('Running migration...'); - await db.query("ALTER TABLE channels ADD COLUMN IF NOT EXISTS type TEXT DEFAULT 'text' CHECK (type IN ('text', 'voice'))"); - console.log('Migration complete: Added type column to channels.'); - process.exit(0); - } catch (err) { - console.error('Migration failed:', err); - process.exit(1); - } -} - -migrate(); diff --git a/Backend/server.js b/Backend/server.js deleted file mode 100644 index 796b5d8..0000000 --- a/Backend/server.js +++ /dev/null @@ -1,271 +0,0 @@ -const express = require('express'); -const http = require('http'); -const { Server } = require('socket.io'); -const cors = require('cors'); -const axios = require('axios'); -require('dotenv').config(); - -const app = express(); -const server = http.createServer(app); -const io = new Server(server, { - cors: { - origin: '*', - methods: ['GET', 'POST'] - } -}); - -const authRoutes = require('./routes/auth'); -const channelRoutes = require('./routes/channels'); -const uploadRoutes = require('./routes/upload'); -const inviteRoutes = require('./routes/invites'); -const rolesRoutes = require('./routes/roles'); -const dmRoutes = require('./routes/dms'); - -app.use(cors()); -app.use(express.json()); - -// Attach IO to request -app.use((req, res, next) => { - req.io = io; - next(); -}); - -// Serve uploads folder based on relative path from server.js -const path = require('path'); -app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); - -app.use('/api/auth', authRoutes); -app.use('/api/channels', channelRoutes); -app.use('/api/upload', uploadRoutes); -app.use('/api/invites', inviteRoutes); -app.use('/api/invites', inviteRoutes); -app.use('/api/roles', rolesRoutes); -app.use('/api/dms', dmRoutes); -app.use('/api/voice', require('./routes/voice')); - -app.get('/', (req, res) => { - res.send('Secure Chat Backend Running'); -}); - -// GIF Routes -const gifCategories = require('./data/gif_categories.json'); - -app.get('/api/gifs/categories', (req, res) => { - res.json(gifCategories); -}); - -app.get('/api/gifs/search', async (req, res) => { - const { q, limit = 8 } = req.query; - const apiKey = process.env.TENOR_API_KEY; - - if (!apiKey) { - // Return mock data or error if no key - console.warn('TENOR_API_KEY missing in .env'); - // Return dummy response to prevent crash - return res.json({ results: [] }); - } - - try { - const response = await axios.get(`https://tenor.googleapis.com/v2/search`, { - params: { - q, - key: apiKey, - limit - } - }); - res.json(response.data); - } catch (err) { - console.error('Tenor API Error:', err.message); - res.status(500).json({ error: 'Failed to fetch GIFs' }); - } -}); - -const redisClient = require('./redis'); -const db = require('./db'); - -io.on('connection', (socket) => { - console.log('User connected:', socket.id); - - socket.on('join_channel', async (data) => { - // Handle both simple string (legacy) and object payload - const channelId = typeof data === 'object' ? data.channelId : data; - const userId = typeof data === 'object' ? data.userId : null; - - socket.join(channelId); - console.log(`User ${socket.id} (ID: ${userId}) joined channel ${channelId}`); - // Load recent messages with reactions - try { - const query = ` - SELECT m.*, u.username, u.public_signing_key, - ( - SELECT json_object_agg(res.emoji, res.data) - FROM ( - SELECT r.emoji, json_build_object( - 'count', COUNT(*)::int, - 'me', BOOL_OR(r.user_id = $2) - ) as data - FROM message_reactions r - WHERE r.message_id = m.id - GROUP BY r.emoji - ) res - ) as reactions - FROM messages m - JOIN users u ON m.sender_id = u.id - WHERE m.channel_id = $1 - ORDER BY m.created_at DESC LIMIT 50 - `; - - const result = await db.query(query, [channelId, userId]); - socket.emit('recent_messages', result.rows.reverse()); - } catch (err) { - console.error('Error fetching messages:', err); - } - }); - - socket.on('send_message', async (data) => { - // data: { channelId, senderId, ciphertext, nonce, signature, keyVersion } - const { channelId, senderId, ciphertext, nonce, signature, keyVersion } = data; - - try { - // Store in DB - const result = await db.query( - `INSERT INTO messages (channel_id, sender_id, ciphertext, nonce, signature, key_version) - VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, created_at`, - [channelId, senderId, ciphertext, nonce, signature, keyVersion] - ); - - const message = { - id: result.rows[0].id, - created_at: result.rows[0].created_at, - ...data - }; - - // Get username and signing key for display/verification - const userRes = await db.query('SELECT username, public_signing_key FROM users WHERE id = $1', [senderId]); - if (userRes.rows.length > 0) { - message.username = userRes.rows[0].username; - message.public_signing_key = userRes.rows[0].public_signing_key; - } - - // Broadcast to channel - io.to(channelId).emit('new_message', message); - } catch (err) { - console.error('Error saving message:', err); - } - }); - - socket.on('typing_start', (data) => { - io.to(data.channelId).emit('typing_start', data); - }); - - socket.on('typing_stop', (data) => { - io.to(data.channelId).emit('typing_stop', data); - }); - - // ... Message handling ... - - // Voice State Handling - // Stores: channelId -> Set(serializedUser: {userId, username}) - // For simplicity: channelId -> [{userId, username}] - - // We need a global variable outside the connection loop, but for this file structure 'socket.on' is inside io.on - // So we need to define it outside. - // See defining it at top of file logic. - - - // Reactions - socket.on('add_reaction', async ({ channelId, messageId, userId, emoji }) => { - try { - if (!messageId || !userId || !emoji) return; - - const result = await db.query( - `INSERT INTO message_reactions (message_id, user_id, emoji) - VALUES ($1, $2, $3) - ON CONFLICT (message_id, user_id, emoji) DO NOTHING`, - [messageId, userId, emoji] - ); - - if (result.rowCount > 0) { - io.to(channelId).emit('reaction_added', { messageId, userId, emoji }); - } - } catch (err) { - console.error('Error adding reaction:', err); - } - }); - - socket.on('remove_reaction', async ({ channelId, messageId, userId, emoji }) => { - try { - if (!messageId || !userId || !emoji) return; - - const result = await db.query( - `DELETE FROM message_reactions - WHERE message_id = $1 AND user_id = $2 AND emoji = $3`, - [messageId, userId, emoji] - ); - - if (result.rowCount > 0) { - io.to(channelId).emit('reaction_removed', { messageId, userId, emoji }); - } - } catch (err) { - console.error('Error removing reaction:', err); - } - }); - - socket.on('join_voice', (channelId) => { }); // Deprecated/Unused placeholder - - socket.on('voice_state_change', (data) => { - // data: { channelId, userId, username, action: 'joined' | 'left' | 'state_update', isMuted, isDeafened } - console.log(`Voice State Update: ${data.username} (${data.userId}) ${data.action} ${data.channelId}`); - - // Update Server State - if (!global.voiceStates) global.voiceStates = {}; - - const currentUsers = global.voiceStates[data.channelId] || []; - - if (data.action === 'joined') { - const existingUser = currentUsers.find(u => u.userId === data.userId); - if (!existingUser) { - currentUsers.push({ - userId: data.userId, - username: data.username, - isMuted: data.isMuted || false, - isDeafened: data.isDeafened || false - }); - } else { - // Update existing on re-join (or just state sync) - existingUser.isMuted = data.isMuted || false; - existingUser.isDeafened = data.isDeafened || false; - } - global.voiceStates[data.channelId] = currentUsers; - } else if (data.action === 'left') { - const index = currentUsers.findIndex(u => u.userId === data.userId); - if (index !== -1) { - currentUsers.splice(index, 1); - global.voiceStates[data.channelId] = currentUsers; - } - } else if (data.action === 'state_update') { - const user = currentUsers.find(u => u.userId === data.userId); - if (user) { - if (data.isMuted !== undefined) user.isMuted = data.isMuted; - if (data.isDeafened !== undefined) user.isDeafened = data.isDeafened; - global.voiceStates[data.channelId] = currentUsers; - } - } - - io.emit('voice_state_update', data); - }); - - socket.on('request_voice_state', () => { - socket.emit('full_voice_state', global.voiceStates || {}); - }); - - socket.on('disconnect', () => { - console.log('User disconnected:', socket.id); - // TODO: Auto-leave logic would require mapping socketId -> userId/channelId - }); -}); - -const PORT = process.env.PORT || 3000; -server.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); -}); diff --git a/CLAUDE.md b/CLAUDE.md index 9ff8800..91355e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,74 @@ -**Update this file when making significant changes.** \ No newline at end of file +**Update this file when making significant changes.** + +See also: [CONVEX_RULES.md](./CONVEX_RULES.md) | [CONVEX_EXAMPLES.md](./CONVEX_EXAMPLES.md) + +## Architecture + +- **Backend**: Convex (reactive database + serverless functions) +- **Frontend**: React + Vite (Electron app) +- **Auth**: Zero-knowledge custom auth via Convex mutations (getSalt, verifyUser, createUserWithProfile) +- **Real-time**: Convex reactive queries (`useQuery` auto-updates all connected clients) +- **Voice/Video**: LiveKit (token generation via Convex Node action) +- **E2E Encryption**: Client-side via Electron IPC (`window.cryptoAPI`) +- **File Storage**: Convex built-in storage (`generateUploadUrl` + `getUrl`) + +## Key Convex Files (convex/) + +- `schema.ts` - Full schema: userProfiles, channels, messages, messageReactions, channelKeys, roles, userRoles, invites, dmParticipants, typingIndicators, voiceStates +- `auth.ts` - getSalt, verifyUser, createUserWithProfile, getPublicKeys +- `channels.ts` - list, get, create, rename, remove (with cascade delete) +- `channelKeys.ts` - uploadKeys, getKeysForUser +- `messages.ts` - list (with reactions + username), send, remove +- `reactions.ts` - add, remove +- `typing.ts` - startTyping, stopTyping, getTyping, cleanExpired (scheduled) +- `dms.ts` - openDM, listDMs +- `invites.ts` - create, use, revoke +- `roles.ts` - list, create, update, remove, listMembers, assign, unassign, getMyPermissions +- `voiceState.ts` - join, leave, updateState, getAll +- `voice.ts` - getToken (Node action, livekit-server-sdk) +- `files.ts` - generateUploadUrl, getFileUrl +- `gifs.ts` - search, categories (Node actions, Tenor API) + +## Frontend Structure (FrontEnd/Electron/src/) + +- `main.jsx` - ConvexProvider + VoiceProvider + HashRouter +- `pages/Login.jsx` - Convex auth (getSalt + verifyUser) +- `pages/Register.jsx` - Convex auth (createUserWithProfile + invite flow) +- `pages/Chat.jsx` - useQuery for channels, channelKeys, DMs +- `components/ChatArea.jsx` - Messages, typing, reactions via Convex queries/mutations +- `components/Sidebar.jsx` - Channel creation, key distribution, invites via Convex +- `contexts/VoiceContext.jsx` - Voice state via Convex + LiveKit room management +- `components/ChannelSettingsModal.jsx` - Channel rename/delete via Convex mutations +- `components/ServerSettingsModal.jsx` - Role management via Convex queries/mutations +- `components/FriendsView.jsx` - User list via Convex query +- `components/DMList.jsx` - DM user picker via Convex query +- `components/GifPicker.jsx` - GIF search via Convex action +- `components/VoiceRoom.jsx` - LiveKit token via Convex action + +## Important Patterns + +- Channel IDs use Convex `_id` (not `id`) - all references use `channel._id` +- Auth: client hashes DAK -> HAK before sending, server does string comparison +- First user bootstrap: createUserWithProfile creates Owner + @everyone roles +- Vite config uses `envDir: '../../'` to pick up root `.env.local` +- `socket.io-client` fully removed, all socket refs replaced with Convex +- No Express backend needed - `Backend/` directory is legacy and can be deleted +- Convex queries are reactive - no need for manual refresh or socket listeners +- File uploads use Convex storage: `generateUploadUrl` -> POST blob -> `getFileUrl` +- Typing indicators use scheduled functions for TTL cleanup + +## Environment Variables + +In `.env.local` at project root: +- `CONVEX_DEPLOYMENT` - Convex deployment URL (set by `npx convex dev`) +- `VITE_CONVEX_URL` - Convex URL for frontend (set by `npx convex dev`) +- `VITE_LIVEKIT_URL` - LiveKit server URL +- `LIVEKIT_API_KEY` - LiveKit API key (used in Convex Node action) +- `LIVEKIT_API_SECRET` - LiveKit API secret (used in Convex Node action) +- `TENOR_API_KEY` - Tenor GIF API key (used in Convex Node action) + +## Running the App + +1. `npm install && npm run install:frontend` +2. `npx convex dev` (starts Convex backend, creates `.env.local`) +3. In another terminal: `cd FrontEnd/Electron && npm run dev` (or `npm run electron:dev`) diff --git a/CONVEX_EXAMPLES.md b/CONVEX_EXAMPLES.md new file mode 100644 index 0000000..4bbdf43 --- /dev/null +++ b/CONVEX_EXAMPLES.md @@ -0,0 +1,307 @@ +# Convex Examples + +Reference implementations for common Convex patterns. + +--- + +## Example: Chat App with AI Responses + +A real-time chat backend demonstrating: +- User and channel management +- Message storage with proper ordering +- OpenAI integration for AI responses +- Background job scheduling + +### Task Requirements +- Allow creating users with names +- Support multiple chat channels +- Enable users to send messages to channels +- Automatically generate AI responses to user messages +- Show recent message history (10 most recent per channel) + +### API Design + +**Public Mutations:** +- `createUser` - Create user with name +- `createChannel` - Create channel with name +- `sendMessage` - Send message and trigger AI response + +**Public Queries:** +- `listMessages` - Get 10 most recent messages (descending) + +**Internal Functions:** +- `generateResponse` - Call OpenAI API +- `loadContext` - Load message history for AI context +- `writeAgentResponse` - Save AI response to database + +### Schema Design +``` +users: { name: string } +channels: { name: string } +messages: { channelId, authorId?, content } + index by_channel +``` + +--- + +### Implementation + +#### package.json +```json +{ + "name": "chat-app", + "version": "1.0.0", + "dependencies": { + "convex": "^1.31.2", + "openai": "^4.79.0" + }, + "devDependencies": { + "typescript": "^5.7.3" + } +} +``` + +#### convex/schema.ts +```typescript +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + channels: defineTable({ + name: v.string(), + }), + + users: defineTable({ + name: v.string(), + }), + + messages: defineTable({ + channelId: v.id("channels"), + authorId: v.optional(v.id("users")), + content: v.string(), + }).index("by_channel", ["channelId"]), +}); +``` + +#### convex/index.ts +```typescript +import { + query, + mutation, + internalQuery, + internalMutation, + internalAction, +} from "./_generated/server"; +import { v } from "convex/values"; +import OpenAI from "openai"; +import { internal } from "./_generated/api"; + +/** + * Create a user with a given name. + */ +export const createUser = mutation({ + args: { + name: v.string(), + }, + returns: v.id("users"), + handler: async (ctx, args) => { + return await ctx.db.insert("users", { name: args.name }); + }, +}); + +/** + * Create a channel with a given name. + */ +export const createChannel = mutation({ + args: { + name: v.string(), + }, + returns: v.id("channels"), + handler: async (ctx, args) => { + return await ctx.db.insert("channels", { name: args.name }); + }, +}); + +/** + * List the 10 most recent messages from a channel in descending creation order. + */ +export const listMessages = query({ + args: { + channelId: v.id("channels"), + }, + returns: v.array( + v.object({ + _id: v.id("messages"), + _creationTime: v.number(), + channelId: v.id("channels"), + authorId: v.optional(v.id("users")), + content: v.string(), + }), + ), + handler: async (ctx, args) => { + const messages = await ctx.db + .query("messages") + .withIndex("by_channel", (q) => q.eq("channelId", args.channelId)) + .order("desc") + .take(10); + return messages; + }, +}); + +/** + * Send a message to a channel and schedule a response from the AI. + */ +export const sendMessage = mutation({ + args: { + channelId: v.id("channels"), + authorId: v.id("users"), + content: v.string(), + }, + returns: v.null(), + handler: async (ctx, args) => { + const channel = await ctx.db.get(args.channelId); + if (!channel) { + throw new Error("Channel not found"); + } + const user = await ctx.db.get(args.authorId); + if (!user) { + throw new Error("User not found"); + } + await ctx.db.insert("messages", { + channelId: args.channelId, + authorId: args.authorId, + content: args.content, + }); + await ctx.scheduler.runAfter(0, internal.index.generateResponse, { + channelId: args.channelId, + }); + return null; + }, +}); + +const openai = new OpenAI(); + +export const generateResponse = internalAction({ + args: { + channelId: v.id("channels"), + }, + returns: v.null(), + handler: async (ctx, args) => { + const context = await ctx.runQuery(internal.index.loadContext, { + channelId: args.channelId, + }); + const response = await openai.chat.completions.create({ + model: "gpt-4o", + messages: context, + }); + const content = response.choices[0].message.content; + if (!content) { + throw new Error("No content in response"); + } + await ctx.runMutation(internal.index.writeAgentResponse, { + channelId: args.channelId, + content, + }); + return null; + }, +}); + +export const loadContext = internalQuery({ + args: { + channelId: v.id("channels"), + }, + returns: v.array( + v.object({ + role: v.union(v.literal("user"), v.literal("assistant")), + content: v.string(), + }), + ), + handler: async (ctx, args) => { + const channel = await ctx.db.get(args.channelId); + if (!channel) { + throw new Error("Channel not found"); + } + const messages = await ctx.db + .query("messages") + .withIndex("by_channel", (q) => q.eq("channelId", args.channelId)) + .order("desc") + .take(10); + + const result = []; + for (const message of messages) { + if (message.authorId) { + const user = await ctx.db.get(message.authorId); + if (!user) { + throw new Error("User not found"); + } + result.push({ + role: "user" as const, + content: `${user.name}: ${message.content}`, + }); + } else { + result.push({ role: "assistant" as const, content: message.content }); + } + } + return result; + }, +}); + +export const writeAgentResponse = internalMutation({ + args: { + channelId: v.id("channels"), + content: v.string(), + }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.db.insert("messages", { + channelId: args.channelId, + content: args.content, + }); + return null; + }, +}); +``` + +#### convex/tsconfig.json +```json +{ + "compilerOptions": { + "allowJs": true, + "strict": true, + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "target": "ESNext", + "lib": ["ES2021", "dom"], + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "isolatedModules": true, + "noEmit": true + }, + "include": ["./**/*"], + "exclude": ["./_generated"] +} +``` + +#### tsconfig.json (root) +```json +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "exclude": ["convex"], + "include": ["**/src/**/*.tsx", "**/src/**/*.ts", "vite.config.ts"] +} +``` diff --git a/CONVEX_RULES.md b/CONVEX_RULES.md new file mode 100644 index 0000000..4bd7ea0 --- /dev/null +++ b/CONVEX_RULES.md @@ -0,0 +1,254 @@ +# Convex Framework Guidelines + +Generic Convex best practices for building backend applications. + +--- + +## Function Guidelines + +### New Function Syntax +ALWAYS use the new function syntax for Convex functions: +```typescript +import { query } from "./_generated/server"; +import { v } from "convex/values"; +export const f = query({ + args: {}, + returns: v.null(), + handler: async (ctx, args) => { + // Function body + }, +}); +``` + +### HTTP Endpoint Syntax +HTTP endpoints are defined in `convex/http.ts` and require an `httpAction` decorator: +```typescript +import { httpRouter } from "convex/server"; +import { httpAction } from "./_generated/server"; +const http = httpRouter(); +http.route({ + path: "/echo", + method: "POST", + handler: httpAction(async (ctx, req) => { + const body = await req.bytes(); + return new Response(body, { status: 200 }); + }), +}); +``` +HTTP endpoints are registered at the exact path you specify. + +### Validators + +Array validator example: +```typescript +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export default mutation({ +args: { + simpleArray: v.array(v.union(v.string(), v.number())), +}, +handler: async (ctx, args) => { + //... +}, +}); +``` + +Discriminated union example: +```typescript +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + results: defineTable( + v.union( + v.object({ + kind: v.literal("error"), + errorMessage: v.string(), + }), + v.object({ + kind: v.literal("success"), + value: v.number(), + }), + ), + ) +}); +``` + +Always use `v.null()` when returning null: +```typescript +export const exampleQuery = query({ + args: {}, + returns: v.null(), + handler: async (ctx, args) => { + return null; + }, +}); +``` + +### Convex Types Reference + +| Convex Type | TS/JS type | Example | Validator | Notes | +|-------------|-------------|---------------|----------------------------------|-------| +| Id | string | `doc._id` | `v.id(tableName)` | | +| Null | null | `null` | `v.null()` | Use `null` instead of `undefined` | +| Int64 | bigint | `3n` | `v.int64()` | -2^63 to 2^63-1 | +| Float64 | number | `3.1` | `v.number()` | IEEE-754 double-precision | +| Boolean | boolean | `true` | `v.boolean()` | | +| String | string | `"abc"` | `v.string()` | UTF-8, <1MB | +| Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | <1MB | +| Array | Array | `[1, 3.2]` | `v.array(values)` | Max 8192 values | +| Object | Object | `{a: "abc"}` | `v.object({property: value})` | Max 1024 entries | +| Record | Record | `{"a": "1"}` | `v.record(keys, values)` | ASCII keys only | + +### Function Registration +- Use `internalQuery`, `internalMutation`, `internalAction` for private functions (only callable by other Convex functions) +- Use `query`, `mutation`, `action` for public API functions +- You CANNOT register functions through the `api` or `internal` objects +- ALWAYS include argument and return validators. Use `returns: v.null()` if no return value + +### Function Calling +- `ctx.runQuery` - call a query from query, mutation, or action +- `ctx.runMutation` - call a mutation from mutation or action +- `ctx.runAction` - call an action from action (only for crossing runtimes V8 to Node) +- All calls take a `FunctionReference`, not the function directly +- Minimize calls from actions to queries/mutations (risk of race conditions) + +When calling functions in the same file, add type annotation: +```typescript +export const g = query({ + args: {}, + returns: v.null(), + handler: async (ctx, args) => { + const result: string = await ctx.runQuery(api.example.f, { name: "Bob" }); + return null; + }, +}); +``` + +### Function References +- Use `api` object for public functions: `api.example.f` +- Use `internal` object for private functions: `internal.example.g` +- File-based routing: `convex/messages/access.ts` -> `api.messages.access.h` + +--- + +## Validator Guidelines +- `v.bigint()` is deprecated - use `v.int64()` instead +- Use `v.record()` for record types. `v.map()` and `v.set()` are not supported + +--- + +## Schema Guidelines +- Define schema in `convex/schema.ts` +- Import schema functions from `convex/server` +- System fields `_creationTime` (v.number()) and `_id` (v.id(tableName)) are automatic +- Name indexes after their fields: `["field1", "field2"]` -> `"by_field1_and_field2"` +- Index fields must be queried in order defined + +--- + +## TypeScript Guidelines +- Use `Id<'tableName'>` from `./_generated/dataModel` for typed IDs +- Record example: `Record, string>` +- Use `as const` for string literals in discriminated unions +- Always define arrays as `const array: Array = [...]` +- Always define records as `const record: Record = {...}` +- Add `@types/node` when using Node.js built-in modules + +--- + +## Query Guidelines +- Do NOT use `filter` - define an index and use `withIndex` instead +- No `.delete()` - use `.collect()` then iterate with `ctx.db.delete(row._id)` +- Use `.unique()` for single document (throws if multiple match) +- For async iteration, use `for await (const row of query)` instead of `.collect()` + +### Ordering +- Default: ascending `_creationTime` +- Use `.order('asc')` or `.order('desc')` +- Indexed queries ordered by index columns + +### Full Text Search +```typescript +const messages = await ctx.db + .query("messages") + .withSearchIndex("search_body", (q) => + q.search("body", "hello hi").eq("channel", "#general"), + ) + .take(10); +``` + +--- + +## Mutation Guidelines +- `ctx.db.replace` - fully replace document (throws if not exists) +- `ctx.db.patch` - shallow merge updates (throws if not exists) + +--- + +## Action Guidelines +- Add `"use node";` at top of files using Node.js modules +- Actions don't have database access (`ctx.db` not available) + +```typescript +import { action } from "./_generated/server"; + +export const exampleAction = action({ + args: {}, + returns: v.null(), + handler: async (ctx, args) => { + return null; + }, +}); +``` + +--- + +## Pagination +```typescript +import { paginationOptsValidator } from "convex/server"; + +export const listWithExtraArg = query({ + args: { paginationOpts: paginationOptsValidator, author: v.string() }, + handler: async (ctx, args) => { + return await ctx.db + .query("messages") + .withIndex("by_author", (q) => q.eq("author", args.author)) + .order("desc") + .paginate(args.paginationOpts); + }, +}); +``` + +`paginationOpts`: `{ numItems: number, cursor: string | null }` + +Returns: `{ page: Doc[], isDone: boolean, continueCursor: string }` + +--- + +## Cron Jobs +- Use `crons.interval` or `crons.cron` only (not hourly/daily/weekly helpers) +- Pass FunctionReference, not the function directly + +```typescript +import { cronJobs } from "convex/server"; +import { internal } from "./_generated/api"; + +const crons = cronJobs(); +crons.interval("job name", { hours: 2 }, internal.crons.myFunction, {}); +export default crons; +``` + +--- + +## File Storage +- `ctx.storage.getUrl()` returns signed URL (null if file doesn't exist) +- Query `_storage` system table for metadata (don't use deprecated `ctx.storage.getMetadata`) + +```typescript +const metadata = await ctx.db.system.get(args.fileId); +// Returns: { _id, _creationTime, contentType?, sha256, size } +``` + +- Store items as `Blob` objects diff --git a/Frontend/Electron/package-lock.json b/Frontend/Electron/package-lock.json index 2407eaf..cd00ca9 100644 --- a/Frontend/Electron/package-lock.json +++ b/Frontend/Electron/package-lock.json @@ -10,14 +10,14 @@ "dependencies": { "@livekit/components-react": "^2.9.17", "@livekit/components-styles": "^1.2.0", + "convex": "^1.31.2", "livekit-client": "^2.16.1", "react": "^19.2.0", "react-dom": "^19.2.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.11.0", "react-syntax-highlighter": "^16.1.0", - "remark-gfm": "^4.0.1", - "socket.io-client": "^4.8.3" + "remark-gfm": "^4.0.1" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -2122,12 +2122,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -3678,6 +3672,496 @@ "dev": true, "license": "MIT" }, + "node_modules/convex": { + "version": "1.31.7", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.31.7.tgz", + "integrity": "sha512-PtNMe1mAIOvA8Yz100QTOaIdgt2rIuWqencVXrb4McdhxBHZ8IJ1eXTnrgCC9HydyilGT1pOn+KNqT14mqn9fQ==", + "license": "Apache-2.0", + "dependencies": { + "esbuild": "0.27.0", + "prettier": "^3.0.0" + }, + "bin": { + "convex": "bin/main.js" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@auth0/auth0-react": "^2.0.1", + "@clerk/clerk-react": "^4.12.8 || ^5.0.0", + "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@auth0/auth0-react": { + "optional": true + }, + "@clerk/clerk-react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/convex/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex/node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -4341,28 +4825,6 @@ "once": "^1.4.0" } }, - "node_modules/engine.io-client": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", - "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.18.3", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -7977,6 +8439,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -8717,34 +9194,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/socket.io-client": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", - "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", - "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -9682,27 +10131,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "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 - } - } - }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -9713,14 +10141,6 @@ "node": ">=8.0" } }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/Frontend/Electron/package.json b/Frontend/Electron/package.json index b8f26bc..53403e3 100644 --- a/Frontend/Electron/package.json +++ b/Frontend/Electron/package.json @@ -33,8 +33,8 @@ "react-markdown": "^10.1.0", "react-router-dom": "^7.11.0", "react-syntax-highlighter": "^16.1.0", - "remark-gfm": "^4.0.1", - "socket.io-client": "^4.8.3" + "convex": "^1.31.2", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/Frontend/Electron/src/assets/icons/index.js b/Frontend/Electron/src/assets/icons/index.js index 1f493f4..8640ae3 100644 --- a/Frontend/Electron/src/assets/icons/index.js +++ b/Frontend/Electron/src/assets/icons/index.js @@ -22,6 +22,7 @@ import EmojiesColored from './emojies_colored.png'; import EmojiesGreyscale from './emojies_greyscale.png'; import TypingIcon from './typing.svg'; import DMIcon from './dm.svg'; +import SpoilerIcon from './spoiler.svg'; export { AddIcon, @@ -47,7 +48,8 @@ export { DeleteIcon, PinIcon, TypingIcon, - DMIcon + DMIcon, + SpoilerIcon }; export const Icons = { @@ -74,5 +76,6 @@ export const Icons = { Delete: DeleteIcon, Pin: PinIcon, Typing: TypingIcon, - DM: DMIcon + DM: DMIcon, + Spoiler: SpoilerIcon }; diff --git a/Frontend/Electron/src/assets/icons/spoiler.svg b/Frontend/Electron/src/assets/icons/spoiler.svg new file mode 100644 index 0000000..78cf429 --- /dev/null +++ b/Frontend/Electron/src/assets/icons/spoiler.svg @@ -0,0 +1 @@ + diff --git a/Frontend/Electron/src/components/ChannelSettingsModal.jsx b/Frontend/Electron/src/components/ChannelSettingsModal.jsx index 54c75ee..129a531 100644 --- a/Frontend/Electron/src/components/ChannelSettingsModal.jsx +++ b/Frontend/Electron/src/components/ChannelSettingsModal.jsx @@ -1,49 +1,41 @@ import React, { useState } from 'react'; +import { useConvex } from 'convex/react'; +import { api } from '../../../../convex/_generated/api'; const ChannelSettingsModal = ({ channel, onClose, onRename, onDelete }) => { const [name, setName] = useState(channel.name); const [activeTab, setActiveTab] = useState('Overview'); + const convex = useConvex(); + const handleSave = async () => { try { - const res = await fetch(`http://localhost:3000/api/channels/${channel.id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name }) - }); - if (res.ok) { - onRename(channel.id, name); - onClose(); - } else { - alert('Failed to update channel'); - } + await convex.mutation(api.channels.rename, { id: channel._id, name }); + onRename(channel._id, name); + onClose(); } catch (err) { console.error(err); + alert('Failed to update channel: ' + err.message); } }; const handleDelete = async () => { if (!confirm('Are you sure you want to delete this channel? This cannot be undone.')) return; - + try { - const res = await fetch(`http://localhost:3000/api/channels/${channel.id}`, { - method: 'DELETE' - }); - if (res.ok) { - onDelete(channel.id); - onClose(); - } else { - alert('Failed to delete channel'); - } + await convex.mutation(api.channels.remove, { id: channel._id }); + onDelete(channel._id); + onClose(); } catch (err) { console.error(err); + alert('Failed to delete channel: ' + err.message); } }; return (
{ }}> {channel.name} Text Channels
- -
setActiveTab('Overview')} style={{ padding: '6px 10px', @@ -86,11 +78,11 @@ const ChannelSettingsModal = ({ channel, onClose, onRename, onDelete }) => { > Overview
- +
-
setActiveTab('Delete')} // Simplify: Just switch content or trigger? UI screenshot implies a tab +
setActiveTab('Delete')} style={{ padding: '6px 10px', borderRadius: '4px', @@ -113,7 +105,7 @@ const ChannelSettingsModal = ({ channel, onClose, onRename, onDelete }) => {

{activeTab === 'Delete' ? 'Delete Channel' : 'Overview'}

-
- -
+ +
{/* Right side spacer like real Discord */}
diff --git a/Frontend/Electron/src/components/ChatArea.jsx b/Frontend/Electron/src/components/ChatArea.jsx index fe88c62..6384c33 100644 --- a/Frontend/Electron/src/components/ChatArea.jsx +++ b/Frontend/Electron/src/components/ChatArea.jsx @@ -1,14 +1,15 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { io } from 'socket.io-client'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useQuery, useMutation, useConvex } from 'convex/react'; +import { api } from '../../../../convex/_generated/api'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { - GifIcon, - EmojieIcon, - StickerIcon, - EmojiesColored, +import { + GifIcon, + EmojieIcon, + StickerIcon, + EmojiesColored, EmojiesGreyscale, EditIcon, ReplyIcon, @@ -16,7 +17,8 @@ import { DeleteIcon, PinIcon, TypingIcon, - AddIcon + AddIcon, + SpoilerIcon } from '../assets/icons'; import PingSound from '../assets/sounds/ping.mp3'; import CategorizedEmojis, { AllEmojis, getEmojiUrl } from '../assets/emojis'; @@ -62,9 +64,6 @@ const LinkPreview = ({ url }) => { const [showControls, setShowControls] = useState(false); const videoRef = useRef(null); - // ... (fetchMeta logic) - - // Helper to extract video ID const getYouTubeId = (link) => { const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; const match = link.match(regExp); @@ -76,7 +75,6 @@ const LinkPreview = ({ url }) => { if (loading || !metadata || (!metadata.title && !metadata.image && !metadata.video)) return null; - // Special handling for direct video files (minimalist UI) if (metadata.video && !isYouTube) { const handlePlayClick = () => { setShowControls(true); @@ -87,24 +85,14 @@ const LinkPreview = ({ url }) => { return (
-