diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..2431052 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +logs/ +*.log diff --git a/backend/package-lock.json b/backend/package-lock.json index 01fa6db..0c8a644 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,10 +19,13 @@ "express": "^5.1.0", "express-rate-limit": "^8.2.0", "express-validator": "^7.3.0", + "express-winston": "^4.2.0", "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.18.3", - "socket.io": "^4.8.1" + "socket.io": "^4.8.1", + "winston": "^3.18.3", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", @@ -570,6 +573,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -583,6 +595,17 @@ "node": ">=12" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", @@ -1337,6 +1360,16 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "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", @@ -1692,6 +1725,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -2129,6 +2168,12 @@ "dev": true, "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/async-mutex": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", @@ -2635,6 +2680,19 @@ "dev": true, "license": "MIT" }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2655,6 +2713,48 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.3.tgz", + "integrity": "sha512-r/wfcFshhORndnDjn3GtNVLA4QL4TAi0A/XIBNuWUIEAVyUBNWYLuckrDz/JM1aQlpIDzKuY5hAYdHcLYgwJsg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3070,6 +3170,12 @@ "dev": true, "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3440,6 +3546,93 @@ "node": ">= 8.0.0" } }, + "node_modules/express-winston": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/express-winston/-/express-winston-4.2.0.tgz", + "integrity": "sha512-EMD74g63nVHi7pFleQw7KHCxiA1pjF5uCwbCfzGqmFxs9KvlDPIVS3cMGpULm6MshExMT9TjC3SqmRGB9kb7yw==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "winston": ">=3.x <4" + } + }, + "node_modules/express-winston/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/express-winston/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/express-winston/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/express-winston/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/express-winston/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/express-winston/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": ">=4" + } + }, + "node_modules/express-winston/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -3471,6 +3664,21 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3559,6 +3767,12 @@ "node": ">=8" } }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -4165,7 +4379,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5106,6 +5319,12 @@ "node": ">=12.0.0" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5191,6 +5410,23 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5391,6 +5627,15 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/mongodb": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", @@ -5646,6 +5891,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5679,6 +5933,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -6044,6 +6307,20 @@ "dev": true, "license": "MIT" }, + "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/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6167,6 +6444,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -6552,6 +6838,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -6586,6 +6881,15 @@ "text-decoder": "^1.1.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/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6802,6 +7106,12 @@ "b4a": "^1.6.4" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6853,6 +7163,15 @@ "tree-kill": "cli.js" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-jest": { "version": "29.4.4", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz", @@ -7185,6 +7504,12 @@ "browserslist": ">= 4.21.0" } }, + "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/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -7284,6 +7609,60 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", + "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index e6f23dc..32d8b48 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,10 +30,13 @@ "express": "^5.1.0", "express-rate-limit": "^8.2.0", "express-validator": "^7.3.0", + "express-winston": "^4.2.0", "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.18.3", - "socket.io": "^4.8.1" + "socket.io": "^4.8.1", + "winston": "^3.18.3", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", diff --git a/backend/src/app.ts b/backend/src/app.ts index 92cc86f..20500f6 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -5,6 +5,7 @@ import express from "express"; import cookieParser from 'cookie-parser'; import { createServer } from "http"; import cors from "cors"; +import logger from "./utils/logger"; import { corsConfig, helmetConfig, @@ -13,6 +14,7 @@ import { requestSizeLimiter } from "./middleware/security"; import { csrfProtection, csrfErrorHandler } from "./middleware/csrf"; +import { requestLogger, errorLogger, notFoundLogger, performanceLogger } from "./middleware/logging"; import healthRoutes from "./routes/health"; import authRoutes from "./routes/auth"; import projectRoutes from "./routes/project"; @@ -30,6 +32,10 @@ export function createApp() { // Initialize Socket.IO service const socketService = new SocketService(server); + // Logging Middleware (early in the chain) + app.use(requestLogger); + app.use(performanceLogger(1000)); // Log requests taking > 1 second + // Security Middleware (order matters!) app.use(helmetConfig); // Security headers app.use(securityHeaders); // Additional custom security headers @@ -68,8 +74,20 @@ export function createApp() { app.use("/api/users", generalLimiter, userRoutes); app.use("/api/branch-protection", branchProtectionRoutes); + // 404 handler + app.use(notFoundLogger); + + // Error logging middleware (before error handlers) + app.use(errorLogger); + // CSRF error handler app.use(csrfErrorHandler); + // Log application start + logger.info('Application initialized', { + nodeEnv: process.env.NODE_ENV, + port: process.env.PORT || 5000, + }); + return { app, server, socketService }; } \ No newline at end of file diff --git a/backend/src/config/db.ts b/backend/src/config/db.ts index 3833491..e56d1d4 100644 --- a/backend/src/config/db.ts +++ b/backend/src/config/db.ts @@ -1,11 +1,18 @@ import mongoose from "mongoose"; +import logger from "../utils/logger"; const connectDB = async () => { try { await mongoose.connect(process.env.MONGO_URI as string); - console.log("✅ MongoDB connected"); + logger.info("MongoDB connected", { + host: mongoose.connection.host, + database: mongoose.connection.name, + }); } catch (error) { - console.error("❌ MongoDB connection failed:", error); + logger.error("MongoDB connection failed", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); process.exit(1); } }; diff --git a/backend/src/index.ts b/backend/src/index.ts index 322bd0b..e9fecfc 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,6 +1,7 @@ import dotenv from "dotenv"; import connectDB from "./config/db"; import { createApp } from "./app"; +import logger from "./utils/logger"; dotenv.config(); @@ -22,6 +23,24 @@ app.get("/", (req, res) => { const PORT = process.env.PORT || 4000; server.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); - console.log(`Socket.IO server ready for connections`); + logger.info(`Server running on http://localhost:${PORT}`); + logger.info(`Socket.IO server ready for connections`); + logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + logger.info('SIGTERM signal received: closing HTTP server'); + server.close(() => { + logger.info('HTTP server closed'); + process.exit(0); + }); +}); + +process.on('SIGINT', () => { + logger.info('SIGINT signal received: closing HTTP server'); + server.close(() => { + logger.info('HTTP server closed'); + process.exit(0); + }); }); diff --git a/backend/src/middleware/logging.ts b/backend/src/middleware/logging.ts new file mode 100644 index 0000000..9e868d8 --- /dev/null +++ b/backend/src/middleware/logging.ts @@ -0,0 +1,127 @@ +import { Request, Response, NextFunction } from 'express'; +import logger from '../utils/logger'; + +/** + * HTTP Request/Response Logging Middleware + * Logs incoming requests and their responses with timing information + */ +export const requestLogger = (req: Request, res: Response, next: NextFunction) => { + const startTime = Date.now(); + + // Log the incoming request + const requestInfo = { + method: req.method, + url: req.url, + ip: req.ip || req.connection.remoteAddress, + userAgent: req.get('user-agent'), + userId: (req as any).user?.id, // If user is authenticated + }; + + logger.http('Incoming request', requestInfo); + + // Capture the original end function + const originalEnd = res.end; + + // Override res.end to log response + res.end = function(chunk?: any, encoding?: any, callback?: any): any { + const duration = Date.now() - startTime; + + const responseInfo = { + method: req.method, + url: req.url, + statusCode: res.statusCode, + duration: `${duration}ms`, + ip: req.ip || req.connection.remoteAddress, + userId: (req as any).user?.id, + }; + + // Log based on status code + if (res.statusCode >= 500) { + logger.error('Request failed', responseInfo); + } else if (res.statusCode >= 400) { + logger.warn('Client error', responseInfo); + } else { + logger.http('Request completed', responseInfo); + } + + // Call the original end function + return originalEnd.call(this, chunk, encoding, callback); + }; + + next(); +}; + +/** + * Error Logging Middleware + * Logs errors with full context and stack traces + */ +export const errorLogger = (err: any, req: Request, res: Response, next: NextFunction) => { + const errorInfo = { + message: err.message, + stack: err.stack, + method: req.method, + url: req.url, + ip: req.ip || req.connection.remoteAddress, + userAgent: req.get('user-agent'), + userId: (req as any).user?.id, + body: req.body, + params: req.params, + query: req.query, + statusCode: err.statusCode || 500, + }; + + // Log error with appropriate level + if (err.statusCode && err.statusCode < 500) { + logger.warn('Client error occurred', errorInfo); + } else { + logger.error('Server error occurred', errorInfo); + } + + // Pass error to next error handler + next(err); +}; + +/** + * Route Not Found Logger + * Logs 404 errors for non-existent routes + */ +export const notFoundLogger = (req: Request, res: Response, next: NextFunction) => { + logger.warn('Route not found', { + method: req.method, + url: req.url, + ip: req.ip || req.connection.remoteAddress, + userAgent: req.get('user-agent'), + }); + + res.status(404).json({ + success: false, + error: 'Route not found', + }); +}; + +/** + * Performance Monitoring Middleware + * Logs slow requests (> threshold) + */ +export const performanceLogger = (threshold: number = 1000) => { + return (req: Request, res: Response, next: NextFunction) => { + const startTime = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - startTime; + + if (duration > threshold) { + logger.warn('Slow request detected', { + method: req.method, + url: req.url, + duration: `${duration}ms`, + threshold: `${threshold}ms`, + statusCode: res.statusCode, + userId: (req as any).user?.id, + }); + } + }); + + next(); + }; +}; diff --git a/backend/src/services/NotificationService.ts b/backend/src/services/NotificationService.ts index cecd70c..267bf25 100644 --- a/backend/src/services/NotificationService.ts +++ b/backend/src/services/NotificationService.ts @@ -1,5 +1,6 @@ import Notification, { INotification } from '../models/Notification'; import { Types } from 'mongoose'; +import logger from '../utils/logger'; export class NotificationService { @@ -36,10 +37,18 @@ export class NotificationService { }); } - console.log(`Notification created and sent to user ${data.recipient}`); + logger.info('Notification created and sent', { + notificationId: notification._id, + recipient: data.recipient, + type: data.type, + }); return notification; } catch (error) { - console.error('Error creating notification:', error); + logger.error('Error creating notification', { + error: error instanceof Error ? error.message : String(error), + recipient: data.recipient, + type: data.type, + }); throw error; } } diff --git a/backend/src/services/SocketService.ts b/backend/src/services/SocketService.ts index 1754738..4e607e7 100644 --- a/backend/src/services/SocketService.ts +++ b/backend/src/services/SocketService.ts @@ -2,6 +2,7 @@ import { Server as HttpServer } from 'http'; import { Server as SocketServer, Socket } from 'socket.io'; import jwt from 'jsonwebtoken'; import Room from '../models/Room'; +import logger from '../utils/logger'; export interface AuthenticatedSocket extends Socket { userId?: string; @@ -50,7 +51,7 @@ class SocketService { if (allowedOrigins.indexOf(origin) !== -1 || allowedRegex.some(r => r.test(origin))) { return callback(null, true); } - console.warn(`Socket.IO CORS blocked origin: ${origin}`); + logger.warn('Socket.IO CORS blocked origin', { origin }); return callback(new Error('Not allowed by CORS')); }, methods: ["GET", "POST"], @@ -84,7 +85,11 @@ class SocketService { private setupEventHandlers() { this.io.on('connection', (socket: AuthenticatedSocket) => { - console.log(`User ${socket.username} connected with socket ID: ${socket.id}`); + logger.info('User connected via Socket.IO', { + userId: socket.userId, + username: socket.username, + socketId: socket.id, + }); if (socket.userId) { this.connectedUsers.set(socket.userId, socket); @@ -113,7 +118,12 @@ class SocketService { await room.save(); - console.log(`${socket.username} joined PR room: ${roomId}`); + logger.info('User joined PR room', { + username: socket.username, + userId: socket.userId, + roomId, + pullRequestId: data.pullRequestId, + }); // Notify others in the room socket.to(roomId).emit('user-joined-room', { @@ -135,7 +145,11 @@ class SocketService { }); } catch (error) { - console.error('Error joining PR room:', error); + logger.error('Error joining PR room', { + error: error instanceof Error ? error.message : String(error), + username: socket.username, + pullRequestId: data.pullRequestId, + }); socket.emit('error', { message: 'Failed to join PR room' }); } }); @@ -151,7 +165,11 @@ class SocketService { username: socket.username }); - console.log(`${socket.username} left PR room: ${roomId}`); + logger.info('User left PR room', { + username: socket.username, + roomId, + pullRequestId: data.pullRequestId, + }); }); // Handle new comments @@ -174,7 +192,12 @@ class SocketService { } }); - console.log(`New comment from ${socket.username} in PR ${data.pullRequestId}`); + logger.info('New comment in PR', { + username: socket.username, + pullRequestId: data.pullRequestId, + hasLineNumber: !!data.lineNumber, + hasFileId: !!data.fileId, + }); }); // Handle comment updates/edits @@ -252,7 +275,11 @@ class SocketService { // Handle disconnection socket.on('disconnect', () => { - console.log(`User ${socket.username} disconnected`); + logger.info('User disconnected from Socket.IO', { + username: socket.username, + userId: socket.userId, + socketId: socket.id, + }); if (socket.userId) { this.connectedUsers.delete(socket.userId); @@ -278,9 +305,9 @@ class SocketService { const userSocket = this.connectedUsers.get(userId); if (userSocket) { userSocket.emit('notification', notification); - console.log(`Notification sent to user ${userId}`); + logger.debug('Notification sent to connected user', { userId }); } else { - console.log(`User ${userId} is not connected, notification stored for later`); + logger.debug('User not connected, notification stored for later', { userId }); } } diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts new file mode 100644 index 0000000..913515b --- /dev/null +++ b/backend/src/utils/logger.ts @@ -0,0 +1,144 @@ +import winston from 'winston'; +import path from 'path'; +import DailyRotateFile from 'winston-daily-rotate-file'; + +// Define log levels +const levels = { + error: 0, + warn: 1, + info: 2, + http: 3, + debug: 4, +}; + +// Define colors for each level +const colors = { + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + debug: 'white', +}; + +// Tell winston about our colors +winston.addColors(colors); + +// Determine log level based on environment +const level = () => { + const env = process.env.NODE_ENV || 'development'; + const isDevelopment = env === 'development'; + return isDevelopment ? 'debug' : 'info'; +}; + +// Define custom formats +const consoleFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.colorize({ all: true }), + winston.format.printf( + (info) => `${info.timestamp} [${info.level}]: ${info.message}${info.stack ? '\n' + info.stack : ''}` + ) +); + +const fileFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), + winston.format.splat(), + winston.format.json() +); + +// Define log directory +const logDir = path.join(process.cwd(), 'logs'); + +// Create transports +const transports: winston.transport[] = []; + +// Console transport (only in development or if explicitly enabled) +if (process.env.NODE_ENV !== 'production' || process.env.ENABLE_CONSOLE_LOGS === 'true') { + transports.push( + new winston.transports.Console({ + format: consoleFormat, + }) + ); +} + +// File transport - All logs +const allLogsTransport: DailyRotateFile = new DailyRotateFile({ + filename: path.join(logDir, 'application-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', // Keep logs for 14 days + format: fileFormat, + level: 'info', +}); + +// File transport - Error logs only +const errorLogsTransport: DailyRotateFile = new DailyRotateFile({ + filename: path.join(logDir, 'error-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '30d', // Keep error logs for 30 days + format: fileFormat, + level: 'error', +}); + +// File transport - HTTP logs +const httpLogsTransport: DailyRotateFile = new DailyRotateFile({ + filename: path.join(logDir, 'http-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '7d', // Keep HTTP logs for 7 days + format: fileFormat, + level: 'http', +}); + +transports.push(allLogsTransport, errorLogsTransport, httpLogsTransport); + +// Create the logger +const logger = winston.createLogger({ + level: level(), + levels, + transports, + // Don't exit on handled exceptions + exitOnError: false, +}); + +// Handle uncaught exceptions +logger.exceptions.handle( + new DailyRotateFile({ + filename: path.join(logDir, 'exceptions-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '30d', + format: fileFormat, + }) +); + +// Handle unhandled promise rejections +logger.rejections.handle( + new DailyRotateFile({ + filename: path.join(logDir, 'rejections-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '30d', + format: fileFormat, + }) +); + +// Create a stream object for Morgan HTTP logger +export const stream = { + write: (message: string) => { + logger.http(message.trim()); + }, +}; + +// Export child logger factory for module-specific logging +export const createModuleLogger = (module: string) => { + return logger.child({ module }); +}; + +export default logger;