diff --git a/backend/package-lock.json b/backend/package-lock.json index af67899..80b182a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,8 +10,8 @@ "license": "MIT", "dependencies": { "@react-oauth/google": "^0.12.2", - "@types/socket.io": "^3.0.1", "@tanstack/react-query": "^5.90.5", + "@types/socket.io": "^3.0.1", "bcryptjs": "^3.0.2", "body-parser": "^1.20.2", "cookie-parser": "^1.4.7", @@ -19,7 +19,9 @@ "crypto": "^1.0.1", "dotenv": "^16.6.1", "express": "^4.21.2", + "express-rate-limit": "^7.5.1", "google-auth-library": "^10.5.0", + "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.2", "morgan": "^1.10.0", @@ -29,8 +31,8 @@ "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "uuid": "^13.0.0", + "winston": "^3.18.3", "zod": "^4.1.12" - "winston": "^3.18.3" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", @@ -1506,10 +1508,18 @@ "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" + } + }, + "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/@tanstack/query-core": { - "version": "5.90.5", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.5.tgz", - "integrity": "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==", + "version": "5.90.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.9.tgz", + "integrity": "sha512-UFOCQzi6pRGeVTVlPNwNdnAvT35zugcIydqjvFUzG62dvz2iVjElmNp/hJkUoM5eqbUPfSU/GJIr/wbvD8bTUw==", "license": "MIT", "funding": { "type": "github", @@ -1517,12 +1527,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.5.tgz", - "integrity": "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==", + "version": "5.90.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.9.tgz", + "integrity": "sha512-Zke2AaXiaSfnG8jqPZR52m8SsclKT2d9//AgE/QIzyNvbpj/Q2ln+FsZjb1j69bJZUouBvX2tg9PHirkTm8arw==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.5" + "@tanstack/query-core": "5.90.9" }, "funding": { "type": "github", @@ -1532,12 +1542,6 @@ "react": "^18 || ^19" } }, - "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", @@ -1683,10 +1687,9 @@ "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, + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", "peer": true, "dependencies": { @@ -2299,6 +2302,13 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -2307,12 +2317,6 @@ "engines": { "node": ">= 12" } - "node_modules/crypto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", - "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", - "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", - "license": "ISC" }, "node_modules/debug": { "version": "2.6.9", @@ -2601,6 +2605,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -2642,6 +2647,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3046,316 +3066,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gaxios/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/gaxios/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gaxios/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gaxios/node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - "node_modules/google-auth-library": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", - "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0", - "gcp-metadata": "^8.0.0", - "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-auth-library/node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-auth-library/node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/google-auth-library/node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gcp-metadata": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", - "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - }, - "node_modules/gtoken": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", - "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", - "license": "MIT", - "dependencies": { - "gaxios": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gtoken/node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/gtoken/node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/gcp-metadata/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/gcp-metadata/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/gcp-metadata/node_modules/gaxios": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", - "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/gcp-metadata/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gcp-metadata/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", - "optional": true - }, - "node_modules/gcp-metadata/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/gcp-metadata/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "optional": true - }, - "node_modules/gcp-metadata/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/gcp-metadata/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3583,6 +3293,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4581,14 +4300,6 @@ }, "engines": { "node": ">= 6" - "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" } }, "node_modules/readdirp": { @@ -5437,7 +5148,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": { diff --git a/backend/package.json b/backend/package.json index 99e60c0..9da8846 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,8 +12,8 @@ "license": "MIT", "dependencies": { "@react-oauth/google": "^0.12.2", - "@types/socket.io": "^3.0.1", "@tanstack/react-query": "^5.90.5", + "@types/socket.io": "^3.0.1", "bcryptjs": "^3.0.2", "body-parser": "^1.20.2", "cookie-parser": "^1.4.7", @@ -21,7 +21,9 @@ "crypto": "^1.0.1", "dotenv": "^16.6.1", "express": "^4.21.2", + "express-rate-limit": "^7.5.1", "google-auth-library": "^10.5.0", + "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.2", "morgan": "^1.10.0", @@ -31,8 +33,8 @@ "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "uuid": "^13.0.0", + "winston": "^3.18.3", "zod": "^4.1.12" - "winston": "^3.18.3" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", diff --git a/backend/src/app.ts b/backend/src/app.ts index 8edda98..f98b7ff 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -5,8 +5,10 @@ import bodyParser from "body-parser"; import cookieParser from "cookie-parser"; import dotenv from "dotenv"; import mongoose from "mongoose"; +import helmet from "helmet"; import { requestLogger } from "./middleware/loggerMiddleware"; import { errorHandler } from "./middleware/errorMiddleware"; +import { generalRateLimiter, authRateLimiter, apiRateLimiter } from "./middleware/rateLimiter"; import bcrypt from "bcryptjs"; import crypto from "crypto"; import nodemailer from "nodemailer"; @@ -36,12 +38,26 @@ dotenv.config(); const app: Application = express(); +app.set("trust proxy", 1); + // ------------------- MIDDLEWARE ------------------- +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"], + imgSrc: ["'self'", "data:", "https:"], + }, + }, + crossOriginEmbedderPolicy: false, +})); app.use(cors()); app.use(morgan("dev")); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json({ limit: "10mb" })); +app.use(bodyParser.urlencoded({ extended: true, limit: "10mb" })); app.use(cookieParser()); +app.use(generalRateLimiter); app.use(requestLogger); // ------------------- MOCK USER DATA ------------------- @@ -69,7 +85,7 @@ const refreshTokens = new Map(); // ------------------- AUTHENTICATION ROUTES ------------------- -app.post("/login", async (req: Request, res: Response) => { +app.post("/login", authRateLimiter, async (req: Request, res: Response) => { const { username, password } = req.body; const user = users.find((u) => u.username === username); @@ -88,7 +104,7 @@ app.post("/login", async (req: Request, res: Response) => { res.json({ accessToken }); }); -app.post("/refresh", (req: Request, res: Response) => { +app.post("/refresh", authRateLimiter, (req: Request, res: Response) => { const token = req.cookies?.refreshToken || req.body.refreshToken; if (!token) { @@ -145,7 +161,7 @@ app.get("/protected", authenticate, (req: AuthRequest, res: Response) => { // ------------------- PASSWORD RESET ROUTES ------------------- -app.post("/api/request-reset", async (req: Request, res: Response) => { +app.post("/api/request-reset", authRateLimiter, async (req: Request, res: Response) => { const { email } = req.body; const user = users.find((u) => u.email === email); @@ -180,7 +196,7 @@ app.post("/api/request-reset", async (req: Request, res: Response) => { } }); -app.post("/api/reset-password/:token", async (req: Request, res: Response) => { +app.post("/api/reset-password/:token", authRateLimiter, async (req: Request, res: Response) => { const { token } = req.params; const { password } = req.body; @@ -208,19 +224,19 @@ app.post("/api/reset-password/:token", async (req: Request, res: Response) => { app.use("/api/health", healthRoutes); // Products — junior, senior, admin -app.use("/api/products", authenticate, authorizeRoles("junior", "senior", "admin"), productRoutes); +app.use("/api/products", apiRateLimiter, authenticate, authorizeRoles("junior", "senior", "admin"), productRoutes); // Users — admin only -app.use("/api/users", authenticate, authorizeRole("admin"), userRoutes); +app.use("/api/users", apiRateLimiter, authenticate, authorizeRole("admin"), userRoutes); // Cart — all authenticated roles -app.use("/api/cart", authenticate, authorizeRoles("junior", "senior", "admin"), cartRoutes); +app.use("/api/cart", apiRateLimiter, authenticate, authorizeRoles("junior", "senior", "admin"), cartRoutes); // Auctions — senior + admin -app.use("/api/auctions", authenticate, authorizeRoles("senior", "admin"), auctionRoutes); +app.use("/api/auctions", apiRateLimiter, authenticate, authorizeRoles("senior", "admin"), auctionRoutes); // Payments — senior + admin -app.use("/api/payments", authenticate, authorizeRoles("senior", "admin"), paymentRoutes); +app.use("/api/payments", apiRateLimiter, authenticate, authorizeRoles("senior", "admin"), paymentRoutes); // Auth app.use("/api", authRoutes); diff --git a/backend/src/logger.ts b/backend/src/logger.ts index 4eacaeb..75e3e4f 100644 --- a/backend/src/logger.ts +++ b/backend/src/logger.ts @@ -1,22 +1,40 @@ import { createLogger, format, transports } from "winston"; const logger = createLogger({ - level: "info", + level: process.env.LOG_LEVEL || "info", format: format.combine( - format.timestamp(), + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), format.errors({ stack: true }), format.splat(), - format.json() + format.json(), + format.metadata({ fillExcept: ["message", "level", "timestamp"] }) ), + defaultMeta: { service: "uniloot-backend" }, transports: [ new transports.Console({ format: format.combine( format.colorize(), - format.simple() + format.printf(({ timestamp, level, message, metadata }) => { + const metaStr = Object.keys(metadata).length ? JSON.stringify(metadata) : ""; + return `${timestamp} [${level}]: ${message} ${metaStr}`; + }) ) }), - //all err msgs goes in application.log - new transports.File({ filename: "application.log" }) + new transports.File({ + filename: "application.log", + format: format.combine( + format.timestamp(), + format.json() + ) + }), + new transports.File({ + filename: "error.log", + level: "error", + format: format.combine( + format.timestamp(), + format.json() + ) + }) ], }); diff --git a/backend/src/middleware/errorMiddleware.ts b/backend/src/middleware/errorMiddleware.ts index 20563c7..6b9117d 100644 --- a/backend/src/middleware/errorMiddleware.ts +++ b/backend/src/middleware/errorMiddleware.ts @@ -7,11 +7,31 @@ export function errorHandler( res: Response, next: NextFunction ) { - logger.error("Error: %s %s | %s", req.method, req.originalUrl, err.stack || err); + const requestId = res.getHeader("x-request-id") || "unknown"; + const errorData = { + requestId, + method: req.method, + url: req.originalUrl, + path: req.path, + statusCode: err.status || err.statusCode || 500, + errorName: err.name || "Error", + errorMessage: err.message || "Internal server error", + stack: err.stack, + ip: req.ip || req.socket.remoteAddress, + userAgent: req.get("user-agent"), + body: req.body, + query: req.query, + params: req.params, + timestamp: new Date().toISOString() + }; - const status = err.status || 500; + logger.error("Request Error", errorData); + + const status = err.status || err.statusCode || 500; res.status(status).json({ + success: false, message: err.message || "Internal server error", - error: process.env.NODE_ENV === "production" ? undefined : err.stack, + requestId, + ...(process.env.NODE_ENV !== "production" && { stack: err.stack }) }); } \ No newline at end of file diff --git a/backend/src/middleware/loggerMiddleware.ts b/backend/src/middleware/loggerMiddleware.ts index dfd7f40..2083054 100644 --- a/backend/src/middleware/loggerMiddleware.ts +++ b/backend/src/middleware/loggerMiddleware.ts @@ -3,15 +3,30 @@ import logger from "../logger"; export const requestLogger = (req: Request, res: Response, next: NextFunction) => { const start = Date.now(); + const requestId = req.headers["x-request-id"] || `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + res.on("finish", () => { const duration = Date.now() - start; - logger.info( - "%s %s %d %dms", - req.method, - req.originalUrl, - res.statusCode, - duration - ); + const logData = { + requestId, + method: req.method, + url: req.originalUrl, + path: req.path, + statusCode: res.statusCode, + duration: `${duration}ms`, + ip: req.ip || req.socket.remoteAddress, + userAgent: req.get("user-agent"), + contentLength: res.get("content-length"), + timestamp: new Date().toISOString() + }; + + if (res.statusCode >= 400) { + logger.warn("HTTP Request", logData); + } else { + logger.info("HTTP Request", logData); + } }); + + res.setHeader("X-Request-ID", requestId); next(); }; \ No newline at end of file diff --git a/backend/src/middleware/rateLimiter.ts b/backend/src/middleware/rateLimiter.ts new file mode 100644 index 0000000..421435e --- /dev/null +++ b/backend/src/middleware/rateLimiter.ts @@ -0,0 +1,65 @@ +import rateLimit from "express-rate-limit"; +import { Request, Response } from "express"; +import logger from "../logger"; + +export const generalRateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + message: "Too many requests from this IP, please try again later.", + standardHeaders: true, + legacyHeaders: false, + handler: (req: Request, res: Response) => { + logger.warn("Rate limit exceeded", { + ip: req.ip, + url: req.originalUrl, + method: req.method, + timestamp: new Date().toISOString() + }); + res.status(429).json({ + success: false, + message: "Too many requests from this IP, please try again later." + }); + } +}); + +export const authRateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5, + message: "Too many authentication attempts, please try again later.", + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests: true, + handler: (req: Request, res: Response) => { + logger.warn("Auth rate limit exceeded", { + ip: req.ip, + url: req.originalUrl, + method: req.method, + timestamp: new Date().toISOString() + }); + res.status(429).json({ + success: false, + message: "Too many authentication attempts, please try again later." + }); + } +}); + +export const apiRateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 200, + message: "Too many API requests, please try again later.", + standardHeaders: true, + legacyHeaders: false, + handler: (req: Request, res: Response) => { + logger.warn("API rate limit exceeded", { + ip: req.ip, + url: req.originalUrl, + method: req.method, + timestamp: new Date().toISOString() + }); + res.status(429).json({ + success: false, + message: "Too many API requests, please try again later." + }); + } +}); +