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:
Bryan1029384756
2026-01-06 17:58:56 -06:00
parent f531301863
commit abedd78893
3795 changed files with 10981 additions and 229 deletions

View 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"
}
]
}

View File

@@ -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",

View File

@@ -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"

View File

@@ -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;

View File

@@ -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
View 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
View 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
View 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
View 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;

View File

@@ -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()
);

View File

@@ -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);

View 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();

View File

@@ -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
});
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.