diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index 0c4405a..04de8b3 100644 --- a/shatter-backend/package-lock.json +++ b/shatter-backend/package-lock.json @@ -10,16 +10,22 @@ "license": "ISC", "dependencies": { "bcryptjs": "^3.0.3", + "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.2", + "react-router-dom": "^7.12.0", + "socket.io": "^4.8.1", "zod": "^4.1.12" }, "devDependencies": { "@eslint/js": "^9.38.0", "@types/bcryptjs": "^2.4.6", "@types/express": "^5.0.5", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.9.2", + "@types/socket.io": "^3.0.1", "eslint": "^9.38.0", "globals": "^16.4.0", "jiti": "^2.6.1", @@ -364,6 +370,12 @@ "node": ">= 8" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -420,6 +432,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -466,6 +487,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -473,13 +505,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.9.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -531,6 +568,16 @@ "@types/node": "*" } }, + "node_modules/@types/socket.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", + "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, "node_modules/@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", @@ -606,7 +653,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -838,7 +884,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -937,6 +982,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -1012,6 +1066,12 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1175,6 +1235,19 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1276,6 +1349,15 @@ "xtend": "^4.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1291,6 +1373,95 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1346,7 +1517,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2057,7 +2227,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -2096,6 +2265,49 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -2145,6 +2357,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2152,6 +2400,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -2407,6 +2661,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -2680,6 +2943,80 @@ "url": "https://opencollective.com/express" } }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "license": "MIT", + "dependencies": { + "react-router": "7.12.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2815,11 +3152,17 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2865,6 +3208,12 @@ "node": ">= 18" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2972,6 +3321,141 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3239,7 +3723,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3276,7 +3759,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -3368,6 +3850,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/shatter-backend/package.json b/shatter-backend/package.json index b3696eb..5c43e30 100644 --- a/shatter-backend/package.json +++ b/shatter-backend/package.json @@ -14,16 +14,22 @@ "description": "", "dependencies": { "bcryptjs": "^3.0.3", + "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.2", + "react-router-dom": "^7.12.0", + "socket.io": "^4.8.1", "zod": "^4.1.12" }, "devDependencies": { "@eslint/js": "^9.38.0", "@types/bcryptjs": "^2.4.6", "@types/express": "^5.0.5", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.9.2", + "@types/socket.io": "^3.0.1", "eslint": "^9.38.0", "globals": "^16.4.0", "jiti": "^2.6.1", diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 70d0daa..98fe109 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -1,4 +1,6 @@ import express from 'express'; +import cors from "cors"; + import userRoutes from './routes/user_route'; // these routes define how to handle requests to /api/users import authRoutes from './routes/auth_routes'; import eventRoutes from './routes/event_routes'; @@ -7,6 +9,16 @@ const app = express(); app.use(express.json()); +app.use(cors({ + origin: "http://localhost:3000", + credentials: true, +})); + +app.use((req, _res, next) => { + req.io = app.get('socketio'); + next(); +}); + app.get('/', (_req, res) => { res.send('Hello'); }); diff --git a/shatter-backend/src/controllers/auth_controller.ts b/shatter-backend/src/controllers/auth_controller.ts index 13bba1f..d796701 100644 --- a/shatter-backend/src/controllers/auth_controller.ts +++ b/shatter-backend/src/controllers/auth_controller.ts @@ -1,6 +1,7 @@ import { Request, Response } from 'express'; import { User } from '../models/user_model'; import { hashPassword, comparePassword } from '../utils/password_hash'; +import { generateToken } from '../utils/jwt_utils'; // Email validation regex const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; @@ -148,11 +149,14 @@ export const login = async (req: Request, res: Response) => { user.lastLogin = new Date(); await user.save(); // save the updated user - // 9 - return success + // 9 - generate JWT token for the user + const token = generateToken(user._id.toString()); + + // 10 - return success with token res.status(200).json({ message: 'Login successful', - userId: user._id - // TODO: figure out a way to add JWT token here + userId: user._id, + token }); } catch (err: any) { diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index 4f06d9b..405b658 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -1,30 +1,40 @@ import { Request, Response } from "express"; import { Event } from "../models/event_model"; -import "../models/participant_model"; - -import {generateEventId, generateJoinCode} from "../utils/event_utils"; +import "../models/participant_model"; +import { generateJoinCode } from "../utils/event_utils"; +import { Participant } from "../models/participant_model"; +import { User } from "../models/user_model"; +import { Types } from "mongoose"; export async function createEvent(req: Request, res: Response) { try { - const { name, description, startDate, endDate, maxParticipant, currentState, createdBy } = req.body; + const { + name, + description, + startDate, + endDate, + maxParticipant, + currentState, + createdBy, + } = req.body; if (!createdBy) { - return res.status(400).json({ success: false, error: "createdBy email is required" }); + return res + .status(400) + .json({ success: false, error: "createdBy email is required" }); } - const eventId = generateEventId(); const joinCode = generateJoinCode(); const event = new Event({ - eventId, name, description, joinCode, startDate, endDate, maxParticipant, - participants: [], + participantIds: [], currentState, createdBy, // required email field }); @@ -37,17 +47,18 @@ export async function createEvent(req: Request, res: Response) { } } - export async function getEventByJoinCode(req: Request, res: Response) { try { const { joinCode } = req.params; if (!joinCode) { - return res.status(400).json({ success: false, error: "joinCode is required" }); + return res + .status(400) + .json({ success: false, error: "joinCode is required" }); } - // Find event by joinCode and populate participants - const event = await Event.findOne({ joinCode }).populate("participants"); + // const event = await Event.findOne({ joinCode }).populate("participantIds"); + const event = await Event.findOne({ joinCode }); if (!event) { return res.status(404).json({ success: false, error: "Event not found" }); @@ -60,4 +71,143 @@ export async function getEventByJoinCode(req: Request, res: Response) { } catch (err: any) { res.status(500).json({ success: false, error: err.message }); } -} \ No newline at end of file +} + +export async function joinEventAsUser(req: Request, res: Response) { + try { + const { name, userId } = req.body; + const { eventId } = req.params; + + if (!userId || !name) + return res.status(400).json({ success: false, msg: "Missing fields" }); + + const event = await Event.findById(eventId); + if (!event) + return res.status(404).json({ success: false, msg: "Event not found" }); + + if (event.participantIds.length >= event.maxParticipant) + return res.status(400).json({ success: false, msg: "Event is full" }); + + let participant = await Participant.findOne({ + userId, + eventId, + }); + + if (participant) { + return res.status(409).json({ success: false, msg: "Already joined" }); + } + + participant = await Participant.create({ + userId, + name, + eventId, + }); + + const participantId = participant._id as Types.ObjectId; + + const eventUpdate = await Event.updateOne( + { _id: eventId }, + { $addToSet: { participantIds: participantId } } + ); + + if (eventUpdate.modifiedCount === 0) { + return res + .status(400) + .json({ success: false, msg: "Already joined this event" }); + } + + // Add event to user history + await User.updateOne( + { _id: userId }, + { $addToSet: { eventHistoryIds: eventId } } + ); + + console.log("Room socket:", eventId); + console.log("Participant data:", { participantId, name }); + + if (!req.io) { + console.error("ERROR: req.io is undefined!"); + } else { + const room = req.io.to(eventId); + console.log("Room object:", room); + + room.emit("participant-joined", { + participantId, + name, + }); + + console.log("Socket event emitted successfully"); + } + + return res.json({ + success: true, + participant, + }); + } catch (e) { + console.error("JOIN EVENT ERROR:", e); + return res.status(500).json({ success: false, msg: "Internal error" }); + } +} + +export async function joinEventAsGuest(req: Request, res: Response) { + try { + const { name } = req.body; + const { eventId } = req.params; + + if (!name) { + return res + .status(400) + .json({ success: false, msg: "Missing guest name" }); + } + + const event = await Event.findById(eventId); + if (!event) { + return res.status(404).json({ success: false, msg: "Event not found" }); + } + + if (event.participantIds.length >= event.maxParticipant) { + return res.status(400).json({ success: false, msg: "Event is full" }); + } + + // Create guest participant (userId is null) + const participant = await Participant.create({ + userId: null, + name, + eventId, + }); + + const participantId = participant._id as Types.ObjectId; + + // Add participant to event + await Event.updateOne( + { _id: eventId }, + { $addToSet: { participantIds: participantId } } + ); + + // Emit socket + console.log("Room socket:", eventId); + console.log("Participant data:", { participantId, name }); + + if (!req.io) { + console.error("ERROR: req.io is undefined!"); + } else { + const room = req.io.to(eventId); + console.log("Room object:", room); + + room.emit("participant-joined", { + participantId, + name, + }); + + console.log("Socket event emitted successfully"); + } + + return res.json({ + success: true, + participant, + }); + } catch (err) { + console.error("JOIN GUEST ERROR:", err); + return res.status(500).json({ success: false, msg: "Internal error" }); + } +} diff --git a/shatter-backend/src/middleware/auth_middleware.ts b/shatter-backend/src/middleware/auth_middleware.ts new file mode 100644 index 0000000..69d4c1f --- /dev/null +++ b/shatter-backend/src/middleware/auth_middleware.ts @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import { Request, Response, NextFunction } from 'express'; +import { verifyToken } from '../utils/jwt_utils'; + +/** + * Extend Express Request type to include user property + * This allows us to attach user info to the request object + */ +declare global { + namespace Express { + interface Request { + user?: { + userId: string; + }; + } + } +} + +/** + * Authentication Middleware + * Verifies JWT token and attaches user info to request + * + * Usage: + * router.get('/protected', authMiddleware, controller); + * + * Request must include: + * Authorization: Bearer + */ +export const authMiddleware = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + // step 1: Get Authorization header + const authHeader = req.headers.authorization; + + if (!authHeader) { + return res.status(401).json({ + error: 'Authorization header missing', + }); + } + + // Step 2: extract token from "Bearer " format + const parts = authHeader.split(' '); + + if (parts.length !== 2) { + return res.status(401).json({ + error: 'Invalid authorization format. Use: Bearer ', + }); + } + + if (parts[0] !== 'Bearer') { + return res.status(401).json({ + error: 'Invalid authorization format. Must start with "Bearer"', + }); + } + + const token = parts[1]; + + if (!token) { + return res.status(401).json({ + error: 'Token is empty', + }); + } + + // Step 3: verify token using JWT utility + const decoded = verifyToken(token) + + // Step 4: Attach user info to request object + req.user = { + userId: decoded.userId, + }; + + // step 5: Continue to next Middleware/controller + next(); + } catch (error: any) { + // Handle specific JWT errors thrown by jwt_utils + if(error?.message === 'Token expired') { + return res.status(401).json({ + error: 'Token expired. Please login again.', + }); + } + + if (error?.message === 'Invalid token') { + return res.status(401).json({ + error: 'Invalid token. Please login again.', + }); + } + + // Generic error + console.error('Auth middleware error:', error); + return res.status(401).json({ + error: 'Authentication failed', + }); + } +}; diff --git a/shatter-backend/src/models/event_model.ts b/shatter-backend/src/models/event_model.ts index fd5d74b..1e6228c 100644 --- a/shatter-backend/src/models/event_model.ts +++ b/shatter-backend/src/models/event_model.ts @@ -1,43 +1,41 @@ import mongoose, { Schema, model, Document, Types } from "mongoose"; -import {User} from "../models/user_model"; +import { User } from "../models/user_model"; import { IParticipant } from "./participant_model"; export interface IEvent extends Document { - eventId: string; name: string; description: string; joinCode: string; startDate: Date; endDate: Date; maxParticipant: number; - participants: mongoose.Types.DocumentArray; + participantIds: Schema.Types.ObjectId[]; currentState: string; createdBy: string; } const EventSchema = new Schema( { - eventId: { type: String, required: true, unique: true }, name: { type: String, required: true }, description: { type: String, required: true }, joinCode: { type: String, required: true, unique: true }, startDate: { type: Date, required: true }, endDate: { type: Date, required: true }, - maxParticipant: { type: Number, required: true }, - participants: [{ type: Types.ObjectId, ref: "Participant" }], + maxParticipant: { type: Number, required: true }, + participantIds: [{ type: Schema.Types.ObjectId, ref: "Participant" }], currentState: { type: String, required: true }, - createdBy: { - type: String, + createdBy: { + type: String, required: true, validate: { validator: async function (email: string) { const user = await User.findOne({ email }); - return !!user; // true if user exists + return !!user; }, - message: "User with this email does not exist" - } - } + message: "User with this email does not exist", + }, + }, }, { timestamps: true, diff --git a/shatter-backend/src/models/participant_model.ts b/shatter-backend/src/models/participant_model.ts index 8de7400..703c970 100644 --- a/shatter-backend/src/models/participant_model.ts +++ b/shatter-backend/src/models/participant_model.ts @@ -1,26 +1,32 @@ import { Schema, model, Document } from "mongoose"; export interface IParticipant extends Document { - participantId: string | null; + userId: Schema.Types.ObjectId | null; name: string; - eventId: string; + eventId: Schema.Types.ObjectId; } const ParticipantSchema = new Schema({ - participantId: { - type: String, + userId: { + type: Schema.Types.ObjectId, + ref: "User", default: null, }, name: { type: String, + ref: "User Name", required: true, }, eventId: { - type: String, + type: Schema.Types.ObjectId, + ref: "Event", required: true, }, }); -export const Participant = model("Participant", ParticipantSchema); +export const Participant = model( + "Participant", + ParticipantSchema +); diff --git a/shatter-backend/src/models/user_model.ts b/shatter-backend/src/models/user_model.ts index 23c1633..a2d29e2 100644 --- a/shatter-backend/src/models/user_model.ts +++ b/shatter-backend/src/models/user_model.ts @@ -1,19 +1,20 @@ // Import Schema and model from the Mongoose library. // - Schema: defines the structure and rules for documents in a collection (like a blueprint). // - model: creates a model (class) that we use in code to read/write those documents. -import { Schema, model } from 'mongoose'; +import { Schema, model } from "mongoose"; // define TS interface for type safety // This helps IDE and compiler know what fields exist on a User export interface IUser { - name: string; - email: string; - passwordHash: string; - lastLogin?: Date; - passwordChangedAt?: Date; - createdAt?: Date; - updatedAt?: Date; + name: string; + email: string; + passwordHash: string; + lastLogin?: Date; + passwordChangedAt?: Date; + createdAt?: Date; + updatedAt?: Date; + eventHistoryIds: Schema.Types.ObjectId[]; } // Create the Mongoose Schema (the database blueprint) @@ -24,8 +25,8 @@ const UserSchema = new Schema( { name: { type: String, - required: true, // field is mandatory; Mongoose will throw error if missing - trim: true // removes extra space at start and end + required: true, // field is mandatory; Mongoose will throw error if missing + trim: true, // removes extra space at start and end }, email: { type: String, @@ -35,35 +36,41 @@ const UserSchema = new Schema( unique: true, index: true, match: [ - /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, - 'Please provide a valid email address' - ] + /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, + "Please provide a valid email address", + ], }, passwordHash: { type: String, required: true, - select: false // Don't return in queries by default + select: false, // Don't return in queries by default }, lastLogin: { type: Date, - default: null + default: null, }, passwordChangedAt: { type: Date, - default: null - } + default: null, + }, + eventHistoryIds: [ + { + type: Schema.Types.ObjectId, + ref: "Event", + }, + ], }, { // timestamps: true automatically adds two fields to each document: // - createdAt: Date when the document was first created // - updatedAt: Date when the document was last modified - timestamps: true + timestamps: true, } ); // Add middleware to auto-update passwordChangedAt -UserSchema.pre('save', function (next) { - if (this.isModified('passwordHash') && !this.isNew) { +UserSchema.pre("save", function (next) { + if (this.isModified("passwordHash") && !this.isNew) { this.passwordChangedAt = new Date(); } next(); @@ -74,4 +81,4 @@ UserSchema.pre('save', function (next) { // "User" is the model name // Mongoose will automatically use "users" as the collection name in MongoDB -export const User = model('User', UserSchema); +export const User = model("User", UserSchema); diff --git a/shatter-backend/src/routes/event_routes.ts b/shatter-backend/src/routes/event_routes.ts index fe95fb1..c890daa 100644 --- a/shatter-backend/src/routes/event_routes.ts +++ b/shatter-backend/src/routes/event_routes.ts @@ -1,11 +1,14 @@ import { Router } from 'express'; -import { createEvent, getEventByJoinCode } from '../controllers/event_controller'; +import { createEvent, getEventByJoinCode, joinEventAsUser, joinEventAsGuest } from '../controllers/event_controller'; const router = Router(); // POST /api/events - create a new event router.post('/createEvent', createEvent); router.get("/event/:joinCode", getEventByJoinCode); +router.post("/:eventId/join/user", joinEventAsUser); +router.post("/:eventId/join/guest", joinEventAsGuest); + export default router; \ No newline at end of file diff --git a/shatter-backend/src/routes/user_route.ts b/shatter-backend/src/routes/user_route.ts index a86e2cf..a3d9720 100644 --- a/shatter-backend/src/routes/user_route.ts +++ b/shatter-backend/src/routes/user_route.ts @@ -1,9 +1,25 @@ -import { Router } from 'express'; +import { Router, Request, Response } from 'express'; import { getUsers, createUser } from '../controllers/user_controller'; +import { authMiddleware } from '../middleware/auth_middleware'; +import { User } from '../models/user_model'; const router = Router(); -router.get('/', getUsers); -router.post('/', createUser); +router.get('/', getUsers); +router.post('/', createUser); + +// Protected route example - returns current user's info +router.get('/me', authMiddleware, async (req: Request, res: Response) => { + try { + const user = await User.findById(req.user?.userId).select('-passwordHash'); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + res.json(user); + } catch (err) { + console.error('GET /api/users/me error:', err); + res.status(500).json({ error: 'Failed to fetch user' }); + } +}); export default router; diff --git a/shatter-backend/src/server.ts b/shatter-backend/src/server.ts index 257ba45..15a3808 100644 --- a/shatter-backend/src/server.ts +++ b/shatter-backend/src/server.ts @@ -1,27 +1,68 @@ -import 'dotenv/config'; -import mongoose from 'mongoose'; -import app from './app'; +import 'dotenv/config'; +import mongoose from 'mongoose'; +import http from 'http'; +import { Server as SocketIOServer } from 'socket.io'; +import app from './app'; // config -const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; -const MONGODB_URI = process.env.MONGO_URI; +const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; +const MONGODB_URI = process.env.MONGO_URI; async function start() { - try { - if (!MONGODB_URI) { - throw new Error('MONGODB_URI is not set'); - } - await mongoose.connect(MONGODB_URI); - console.log('Successfully connected to MongoDB'); - - // start listening for incoming HTTP requests on chosen port - app.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); - }); - } catch (err) { - console.error('Failed to start server:', err); - process.exit(1); + try { + if (!MONGODB_URI) { + throw new Error("MONGODB_URI is not set"); } + await mongoose.connect(MONGODB_URI); + console.log("Successfully connected to MongoDB"); + + // // start listening for incoming HTTP requests on chosen port + // app.listen(PORT, () => { + // console.log(`Server running on http://localhost:${PORT}`); + // }); + + // Create HTTP server from Express app + const httpServer = http.createServer(app); + + // Setup Socket.IO + const io = new SocketIOServer(httpServer, { + cors: { + origin: "http://localhost:3000", // React frontend + methods: ["GET", "POST"], + credentials: true, + }, + transports: ["websocket", "polling"], // fallback to polling + }); + + app.set('socketio', io); + + // Socket.IO connection handler + io.on("connection", (socket) => { + console.log("Client connected:", socket.id); + + socket.on("join-event-room", (eventId: string) => { + socket.join(eventId); + console.log(`Socket ${socket.id} joined room ${eventId}`); + }); + + socket.on("leave-event-room", (eventId: string) => { + socket.leave(eventId); + console.log(`Socket ${socket.id} left room ${eventId}`); + }); + + socket.on("disconnect", () => { + console.log("Client disconnected:", socket.id); + }); + }); + + // Start server + httpServer.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); + }); + } catch (err) { + console.error("Failed to start server:", err); + process.exit(1); + } } start(); diff --git a/shatter-backend/src/types/express/index.d.ts b/shatter-backend/src/types/express/index.d.ts new file mode 100644 index 0000000..9725032 --- /dev/null +++ b/shatter-backend/src/types/express/index.d.ts @@ -0,0 +1,9 @@ +import { Server as SocketIOServer } from "socket.io"; + +declare global { + namespace Express { + interface Request { + io: SocketIOServer; + } + } +} diff --git a/shatter-backend/src/utils/jwt_utils.ts b/shatter-backend/src/utils/jwt_utils.ts new file mode 100644 index 0000000..5442035 --- /dev/null +++ b/shatter-backend/src/utils/jwt_utils.ts @@ -0,0 +1,70 @@ +import jwt from 'jsonwebtoken'; + +// Get JWT secret from .env +const JWT_SECRET = process.env.JWT_SECRET || ''; +const JWT_EXPIRATION = '30d'; // set token to expire in 30 days + +// Validate that secret actually exists +if (!JWT_SECRET) { + console.warn('WARNING: JWT_SECRET not set in environment variables!'); +} + +/** + * Generate a JWT token for a user + * + * @param userId - The user's MongoDB _id + * @returns A signed JWT token string + * + * @example + * const token = generateToken('673abc123def456'); + * // Returns: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + */ +export const generateToken = (userId: string): string => { + try{ + // create and sign the token + const token = jwt.sign( + { userId }, // payload - data we want to store + JWT_SECRET, // Secret key - proves token is real + { expiresIn: JWT_EXPIRATION} // Options - token expires in 30 days + ); + + return token; + + } catch (error) { + console.error('Error generating JWT token:', error); + throw new Error('Failed to generate authentication token'); + } +}; + +/** + * Verify a JWT token and extract the userId + * + * @param token - The JWT token string to verify + * @returns Object containing the userId + * @throws Error if token is invalid or expired + * + * @example + * const decoded = verifyToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'); + * console.log(decoded.userId); // "673abc123def456" + */ +export const verifyToken = (token: string): { userId: string } => { + try { + // verify the token signature and decode + const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }; + + return decoded; + + } catch (error) { + // Handle specific JWT errors + if (error instanceof jwt.TokenExpiredError) { + throw new Error('Token expired'); + } + if (error instanceof jwt.JsonWebTokenError) { + throw new Error('Invalid token'); + } + + // Generic error + console.error('Error verifying JWT token:', error); + throw new Error('Token verification failed'); + } +}; diff --git a/shatter-backend/src/utils/test_jwt.ts b/shatter-backend/src/utils/test_jwt.ts new file mode 100644 index 0000000..edd99e8 --- /dev/null +++ b/shatter-backend/src/utils/test_jwt.ts @@ -0,0 +1,51 @@ +import dotenv from 'dotenv'; +// Load environment variables +dotenv.config(); + +import { generateToken, verifyToken } from './jwt_utils'; + + +function testJWT() { + console.log('๐Ÿงช Testing JWT Utilities...\n'); + + // Test 1: Generate a token + console.log('๐Ÿ“ Test 1: Generating token...'); + const userId = '673abc123def456789'; + const token = generateToken(userId); + console.log('โœ… Token generated:', token); + console.log(' Token length:', token.length, 'characters\n'); + + // Test 2: Verify a valid token + console.log('๐Ÿ“ Test 2: Verifying valid token...'); + try { + const decoded = verifyToken(token); + console.log('โœ… Token verified successfully'); + console.log(' Decoded userId:', decoded.userId); + console.log(' Matches original?', decoded.userId === userId ? 'YES โœ…' : 'NO โŒ\n'); + } catch (error: any) { + console.log('โŒ Verification failed:', error.message); + } + + // Test 3: Try to verify an invalid token + console.log('\n๐Ÿ“ Test 3: Testing invalid token...'); + try { + const fakeToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake.signature'; + verifyToken(fakeToken); + console.log('โŒ Invalid token was accepted (this should not happen!)'); + } catch (error: any) { + console.log('โœ… Invalid token correctly rejected'); + console.log(' Error:', error.message); + } + + // Test 4: Token structure + console.log('\n๐Ÿ“ Test 4: Token structure...'); + const parts = token.split('.'); + console.log('โœ… Token has', parts.length, 'parts (should be 3)'); + console.log(' Part 1 (Header):', parts[0].substring(0, 20) + '...'); + console.log(' Part 2 (Payload):', parts[1].substring(0, 20) + '...'); + console.log(' Part 3 (Signature):', parts[2].substring(0, 20) + '...'); + + console.log('\n๐ŸŽ‰ All JWT tests completed!'); +} + +testJWT(); diff --git a/shatter-backend/tsconfig.json b/shatter-backend/tsconfig.json index bfaac61..c997406 100644 --- a/shatter-backend/tsconfig.json +++ b/shatter-backend/tsconfig.json @@ -8,7 +8,8 @@ "sourceMap": true, "outDir": "./dist", "rootDir": "./", - "lib": ["ES2021"] + "lib": ["ES2021"], + "typeRoots": ["./src/types", "./node_modules/@types"] }, "include": ["src/**/*", "api/**/*"], "exclude": ["node_modules", "dist"]