feat: Add a large collection of emoji and other frontend assets, including a sound file, and a backend package.json.
This commit is contained in:
240
Backend/data/gif_categories.json
Normal file
240
Backend/data/gif_categories.json
Normal file
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
422
Backend/package-lock.json
generated
422
Backend/package-lock.json
generated
@@ -9,14 +9,32 @@
|
||||
"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",
|
||||
@@ -114,6 +132,29 @@
|
||||
"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",
|
||||
@@ -147,6 +188,23 @@
|
||||
"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",
|
||||
@@ -185,6 +243,36 @@
|
||||
"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",
|
||||
@@ -194,6 +282,33 @@
|
||||
"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",
|
||||
@@ -264,6 +379,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -416,6 +540,21 @@
|
||||
"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",
|
||||
@@ -495,6 +634,63 @@
|
||||
"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",
|
||||
@@ -583,6 +779,21 @@
|
||||
"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",
|
||||
@@ -652,6 +863,42 @@
|
||||
"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",
|
||||
@@ -707,12 +954,94 @@
|
||||
"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",
|
||||
@@ -924,6 +1253,12 @@
|
||||
"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",
|
||||
@@ -939,6 +1274,18 @@
|
||||
"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",
|
||||
@@ -963,6 +1310,20 @@
|
||||
"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",
|
||||
@@ -995,6 +1356,26 @@
|
||||
"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",
|
||||
@@ -1226,6 +1607,23 @@
|
||||
"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",
|
||||
@@ -1235,6 +1633,18 @@
|
||||
"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",
|
||||
@@ -1249,6 +1659,12 @@
|
||||
"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",
|
||||
@@ -1264,6 +1680,12 @@
|
||||
"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",
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
"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"
|
||||
|
||||
@@ -11,14 +11,81 @@ function generateFakeSalt(username) {
|
||||
}
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
const { username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys } = req.body;
|
||||
const { username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys, inviteCode } = req.body;
|
||||
|
||||
try {
|
||||
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]
|
||||
);
|
||||
res.json({ success: true, userId: result.rows[0].id });
|
||||
// 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
|
||||
@@ -50,7 +117,7 @@ router.post('/login/verify', async (req, res) => {
|
||||
|
||||
try {
|
||||
const result = await db.query(
|
||||
'SELECT id, hashed_auth_key, encrypted_master_key, encrypted_private_keys FROM users WHERE username = $1',
|
||||
'SELECT id, hashed_auth_key, encrypted_master_key, encrypted_private_keys, public_identity_key FROM users WHERE username = $1',
|
||||
[username]
|
||||
);
|
||||
|
||||
@@ -66,7 +133,8 @@ router.post('/login/verify', async (req, res) => {
|
||||
success: true,
|
||||
userId: user.id,
|
||||
encryptedMK: user.encrypted_master_key,
|
||||
encryptedPrivateKeys: user.encrypted_private_keys
|
||||
encryptedPrivateKeys: user.encrypted_private_keys,
|
||||
publicKey: user.public_identity_key // Return Public Key
|
||||
});
|
||||
} else {
|
||||
res.status(401).json({ error: 'Invalid credentials' });
|
||||
@@ -77,4 +145,14 @@ router.post('/login/verify', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/users/public-keys', async (req, res) => {
|
||||
try {
|
||||
const result = await db.query('SELECT id, public_identity_key FROM users');
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -12,4 +12,149 @@ router.get('/', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
75
Backend/routes/invites.js
Normal file
75
Backend/routes/invites.js
Normal file
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
213
Backend/routes/roles.js
Normal file
213
Backend/routes/roles.js
Normal file
@@ -0,0 +1,213 @@
|
||||
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;
|
||||
41
Backend/routes/upload.js
Normal file
41
Backend/routes/upload.js
Normal file
@@ -0,0 +1,41 @@
|
||||
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;
|
||||
64
Backend/routes/voice.js
Normal file
64
Backend/routes/voice.js
Normal file
@@ -0,0 +1,64 @@
|
||||
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;
|
||||
@@ -8,22 +8,32 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
hashed_auth_key TEXT NOT NULL,
|
||||
public_identity_key TEXT NOT NULL,
|
||||
public_signing_key TEXT NOT NULL,
|
||||
encrypted_private_keys TEXT NOT NULL, -- Added this column
|
||||
encrypted_private_keys TEXT NOT NULL,
|
||||
is_admin BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
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',
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
type TEXT DEFAULT 'text' CHECK (type IN ('text', 'voice')),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
permissions JSONB
|
||||
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 (
|
||||
@@ -42,5 +52,16 @@ CREATE TABLE IF NOT EXISTS messages (
|
||||
nonce TEXT NOT NULL,
|
||||
signature TEXT NOT NULL,
|
||||
key_version INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
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()
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
const db = require('../db');
|
||||
|
||||
async function initDb() {
|
||||
@@ -7,18 +8,33 @@ async function initDb() {
|
||||
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);
|
||||
|
||||
// Seed Channels
|
||||
const channels = ['general', 'random'];
|
||||
for (const name of channels) {
|
||||
await db.query(
|
||||
`INSERT INTO channels (name) VALUES ($1) ON CONFLICT (name) DO NOTHING`,
|
||||
[name]
|
||||
);
|
||||
}
|
||||
console.log('Channels seeded.');
|
||||
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);
|
||||
|
||||
17
Backend/scripts/migrate_voice.js
Normal file
17
Backend/scripts/migrate_voice.js
Normal file
@@ -0,0 +1,17 @@
|
||||
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();
|
||||
@@ -2,6 +2,7 @@ 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();
|
||||
@@ -15,36 +16,104 @@ const io = new Server(server, {
|
||||
|
||||
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');
|
||||
|
||||
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/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 (channelId) => {
|
||||
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} joined channel ${channelId}`);
|
||||
// Load recent messages
|
||||
console.log(`User ${socket.id} (ID: ${userId}) joined channel ${channelId}`);
|
||||
// Load recent messages with reactions
|
||||
try {
|
||||
const result = await db.query(
|
||||
`SELECT m.*, u.username, u.public_signing_key
|
||||
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`,
|
||||
[channelId]
|
||||
);
|
||||
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);
|
||||
@@ -83,12 +152,114 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('typing', (data) => {
|
||||
socket.to(data.channelId).emit('user_typing', { username: data.username });
|
||||
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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
BIN
Backend/uploads/1767493519909-231561716.enc
Normal file
BIN
Backend/uploads/1767493519909-231561716.enc
Normal file
Binary file not shown.
BIN
Backend/uploads/1767493677800-686548739.enc
Normal file
BIN
Backend/uploads/1767493677800-686548739.enc
Normal file
Binary file not shown.
BIN
Backend/uploads/1767496855215-848429954.enc
Normal file
BIN
Backend/uploads/1767496855215-848429954.enc
Normal file
Binary file not shown.
BIN
Backend/uploads/1767497306694-744544719.enc
Normal file
BIN
Backend/uploads/1767497306694-744544719.enc
Normal file
Binary file not shown.
BIN
Backend/uploads/1767571577033-791228774.enc
Normal file
BIN
Backend/uploads/1767571577033-791228774.enc
Normal file
Binary file not shown.
BIN
Backend/uploads/1767731525251-885122068.enc
Normal file
BIN
Backend/uploads/1767731525251-885122068.enc
Normal file
Binary file not shown.
BIN
Backend/uploads/1767736657939-176839166.enc
Normal file
BIN
Backend/uploads/1767736657939-176839166.enc
Normal file
Binary file not shown.
BIN
Backend/uploads/1767736668121-484246219.enc
Normal file
BIN
Backend/uploads/1767736668121-484246219.enc
Normal file
Binary file not shown.
Reference in New Issue
Block a user