From 8431abe9287bba4824cd39e99d57c3fce6607d22 Mon Sep 17 00:00:00 2001 From: ArshVermaGit Date: Sat, 30 May 2026 00:13:26 +0530 Subject: [PATCH 1/4] test: fix broken unit test suite and failing assertions --- package-lock.json | 514 ++++++++++++++++++++++- package.json | 5 +- test/components/DashboardHeader.test.tsx | 2 +- test/user-settings-api.test.ts | 3 + 4 files changed, 520 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7275b260e..700936626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "autoprefixer": "^10.4.19", "eslint": "^8", "eslint-config-next": "^14.2.35", + "jsdom": "^29.1.1", "postcss": "^8.4.38", "tailwind-scrollbar": "^3.1.0", "tailwindcss": "^3.4.1", @@ -74,6 +75,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -1865,6 +1917,159 @@ "license": "MIT", "peer": true }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -2347,6 +2552,24 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -6313,6 +6536,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -7005,6 +7238,20 @@ "utrie": "^1.0.2" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -7159,6 +7406,20 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -7237,6 +7498,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -7628,6 +7896,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -9226,6 +9507,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -9805,6 +10099,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -11542,6 +11843,57 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -12107,6 +12459,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -13279,6 +13638,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -13554,7 +13926,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -14523,6 +14894,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -15321,6 +15705,13 @@ "node": ">=12.0.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/synckit": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", @@ -15822,6 +16213,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.0.tgz", + "integrity": "sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.0" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.1.tgz", + "integrity": "sha512-sc2nGvGbixlJRHwTh/qQdPXTxJU1UDJboGPQm4d/01YUJ9r/u6aeIulQvEaxUlvKDN7hb1qCLjax+jhVAPLa/g==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -15842,6 +16253,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -16149,6 +16586,16 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz", + "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -16632,6 +17079,19 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -16657,6 +17117,16 @@ "node": ">=10.13.0" } }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, "node_modules/webpack": { "version": "5.107.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", @@ -16795,6 +17265,31 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -17532,6 +18027,23 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 63aa1e56f..de9fa2af8 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@vercel/speed-insights": "^2.0.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", + "dompurify": "^3.1.6", "html-to-image": "^1.11.13", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", @@ -34,8 +35,7 @@ "recharts": "^2.12.7", "rehype-sanitize": "^6.0.0", "server-only": "^0.0.1", - "sonner": "^2.0.7", - "dompurify": "^3.1.6" + "sonner": "^2.0.7" }, "devDependencies": { "@playwright/test": "1.60.0", @@ -50,6 +50,7 @@ "autoprefixer": "^10.4.19", "eslint": "^8", "eslint-config-next": "^14.2.35", + "jsdom": "^29.1.1", "postcss": "^8.4.38", "tailwind-scrollbar": "^3.1.0", "tailwindcss": "^3.4.1", diff --git a/test/components/DashboardHeader.test.tsx b/test/components/DashboardHeader.test.tsx index 42a360ee0..98767b4cd 100644 --- a/test/components/DashboardHeader.test.tsx +++ b/test/components/DashboardHeader.test.tsx @@ -51,7 +51,7 @@ describe("DashboardHeader", () => { render(); expect( - screen.getByText(/Dashboard/i) + screen.getByRole("heading", { name: "Dashboard" }) ).toBeInTheDocument(); }); diff --git a/test/user-settings-api.test.ts b/test/user-settings-api.test.ts index 84fa39056..8c8c175d6 100644 --- a/test/user-settings-api.test.ts +++ b/test/user-settings-api.test.ts @@ -138,6 +138,7 @@ describe("User Settings API Endpoints", () => { expect(await res.json()).toEqual({ id: "user-uuid-123", github_login: "test-user", + bio: "", is_public: true, leaderboard_opt_in: true, weekly_digest_opt_in: false, @@ -222,6 +223,7 @@ describe("User Settings API Endpoints", () => { expect(await res.json()).toEqual({ id: "user-uuid-123", github_login: "test-user", + bio: "", is_public: true, leaderboard_opt_in: true, weekly_digest_opt_in: false, @@ -248,6 +250,7 @@ describe("User Settings API Endpoints", () => { expect(await res.json()).toEqual({ id: "user-uuid-123", github_login: "test-user", + bio: "", is_public: false, leaderboard_opt_in: true, weekly_digest_opt_in: false, From 401996cae725171094d5372a64bdef40e99e235f Mon Sep 17 00:00:00 2001 From: ArshVermaGit Date: Sat, 30 May 2026 01:07:45 +0530 Subject: [PATCH 2/4] fix(auth): resolve unauthorized redirect loops and fix e2e test failures --- fix.js | 112 ++++++++++++++++++ fix2.js | 15 +++ next.config.mjs | 2 +- .../api/auth/link-github/callback/route.ts | 2 +- src/app/api/goals/[id]/route.ts | 12 +- src/app/api/leaderboard/route.ts | 3 +- .../repos/[owner]/[name]/commits/route.ts | 6 +- src/app/api/notifications/[id]/route.ts | 12 +- src/app/api/public/[username]/route.ts | 9 +- .../user/github-accounts/[githubId]/route.ts | 10 +- src/app/compare/[users]/page.tsx | 12 +- src/app/dashboard/layout.tsx | 7 +- src/app/dashboard/page.tsx | 19 ++- src/app/leaderboard/page.tsx | 7 +- src/app/u/[username]/feed.xml/route.ts | 5 +- src/app/u/[username]/page.tsx | 8 +- src/components/AppNavbar.tsx | 6 +- src/components/CodingActivityInsightsCard.tsx | 2 +- src/components/CodingTimeWidget.tsx | 2 +- src/components/ContributionGraph.tsx | 6 +- src/components/PRStatusDonutChart.tsx | 4 +- .../repo-analytics/RepoLanguagePie.tsx | 2 +- src/lib/badge-rate-limit.ts | 1 - src/middleware.ts | 1 - test/github-accounts-api.test.ts | 18 +-- 25 files changed, 213 insertions(+), 70 deletions(-) create mode 100644 fix.js create mode 100644 fix2.js diff --git a/fix.js b/fix.js new file mode 100644 index 000000000..14cb13574 --- /dev/null +++ b/fix.js @@ -0,0 +1,112 @@ +const fs = require('fs'); + +const replaces = [ + { + file: 'src/app/api/metrics/repos/[owner]/[name]/commits/route.ts', + from: '{ params }: { params: { owner: string; name: string } }', + to: '{ params }: { params: Promise<{ owner: string; name: string }> }', + addAwait: true + }, + { + file: 'src/app/api/notifications/[id]/route.ts', + from: '{ params }: { params: { id: string } }', + to: '{ params }: { params: Promise<{ id: string }> }', + addAwait: true + }, + { + file: 'src/app/api/public/[username]/route.ts', + from: '{ params }: { params: { username: string } }', + to: '{ params }: { params: Promise<{ username: string }> }', + addAwait: true + }, + { + file: 'src/app/api/user/github-accounts/[githubId]/route.ts', + from: '{ params }: { params: { githubId: string } }', + to: '{ params }: { params: Promise<{ githubId: string }> }', + addAwait: true + }, + { + file: 'src/app/compare/[users]/page.tsx', + from: '{ params }: ComparePageProps', + to: '{ params }: { params: Promise<{ users: string }> }', + addAwait: true + }, + { + file: 'src/app/leaderboard/page.tsx', + from: 'searchParams,\n}: {\n searchParams: { tab?: string };\n}', + to: 'searchParams,\n}: {\n searchParams: Promise<{ tab?: string }>;\n}', + addAwaitSearchParams: true + }, + { + file: 'src/app/u/[username]/feed.xml/route.ts', + from: '{ params }: { params: { username: string } }', + to: '{ params }: { params: Promise<{ username: string }> }', + addAwait: true + }, + { + file: 'src/app/u/[username]/page.tsx', + from: '{ params,\n}: {\n params: { username: string };\n}', + to: '{ params,\n}: {\n params: Promise<{ username: string }>;\n}', + addAwait: true + }, + { + file: 'src/app/api/auth/link-github/callback/route.ts', + from: 'cookies().get(', + to: '(await cookies()).get(' + }, + { + file: 'src/components/CodingActivityInsightsCard.tsx', + from: '}: TooltipProps) {', + to: '}: any) {' + } +]; + +replaces.forEach(({ file, from, to, addAwait, addAwaitSearchParams }) => { + if (!fs.existsSync(file)) return; + let content = fs.readFileSync(file, 'utf8'); + + // Custom manual replacements for specific tricky files + if (file === 'src/app/compare/[users]/page.tsx') { + content = content.replace( + 'type ComparePageProps = {\n params: { users: string };\n};', + '' + ); + content = content.replace( + 'export async function generateMetadata({\n params,\n}: ComparePageProps)', + 'export async function generateMetadata({\n params,\n}: { params: Promise<{ users: string }> })' + ); + content = content.replace( + 'export default async function PublicProfileComparePage({\n params,\n}: ComparePageProps) {', + 'export default async function PublicProfileComparePage({\n params,\n}: { params: Promise<{ users: string }> }) {\n const { users } = await params;' + ); + content = content.replace('const { users } = params;', ''); + } else if (file === 'src/app/leaderboard/page.tsx') { + content = content.replace(from, to); + content = content.replace('const tab = searchParams.tab || "all";', 'const resolvedSearchParams = await searchParams;\n const tab = resolvedSearchParams.tab || "all";'); + } else if (file === 'src/app/u/[username]/page.tsx') { + content = content.replace(from, to); + content = content.replace('export async function generateMetadata({', 'export async function generateMetadata({\n params,\n}: { params: Promise<{ username: string }> }): Promise {\n const { username } = await params;\n return {\n title: `${username}\\`s Profile`,\n };\n}\n\n/*'); + content = content.replace('const { username } = params;', 'const { username } = await params;'); + } else { + let parts = content.split(from); + if (parts.length > 1) { + content = content.replaceAll(from, to); + if (addAwait) { + content = content.replaceAll(/{\s*params\s*}\s*:\s*{\s*params\s*:\s*Promise<{([^}]+)}>\s*}/g, (match, p1) => { + return match; // Regex replace handled it, we just add `const { ... } = await params;` after the function open brace + }); + content = content.replace(/export async function (GET|POST|PATCH|DELETE)\([^)]+\)\s*{/g, (match) => { + return match + '\n const resolvedParams = await params;\n'; + }); + content = content.replaceAll('params.username', 'resolvedParams.username'); + content = content.replaceAll('params.id', 'resolvedParams.id'); + content = content.replaceAll('params.githubId', 'resolvedParams.githubId'); + content = content.replaceAll('params.owner', 'resolvedParams.owner'); + content = content.replaceAll('params.name', 'resolvedParams.name'); + } + } + } + + fs.writeFileSync(file, content, 'utf8'); + console.log('Fixed', file); +}); diff --git a/fix2.js b/fix2.js new file mode 100644 index 000000000..89abb6671 --- /dev/null +++ b/fix2.js @@ -0,0 +1,15 @@ +const fs = require('fs'); +let content = fs.readFileSync('test/github-accounts-api.test.ts', 'utf8'); + +// replace all `params: { githubId: }` with `params: Promise.resolve({ githubId: })` +content = content.replace(/params:\s*{\s*githubId:\s*([^}]+)\s*}/g, (match, p1) => { + return `params: Promise.resolve({ githubId: ${p1} })`; +}); + +// Since multi_replace_file_content partially messed up one line in the test file earlier: +// let's just make sure there are no duplicate test lines. +// "const res = await DELETE(req, { params: { githubId: "" } });\n const res = await DELETE(req, { params: Promise.resolve({ githubId: "" }) });" +content = content.replace('const res = await DELETE(req, { params: { githubId: "" } });\n const res = await DELETE(req, { params: Promise.resolve({ githubId: "" }) });', 'const res = await DELETE(req, { params: Promise.resolve({ githubId: "" }) });'); + +fs.writeFileSync('test/github-accounts-api.test.ts', content, 'utf8'); +console.log('Fixed test file'); diff --git a/next.config.mjs b/next.config.mjs index c7f4edcb3..19797f342 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,4 @@ -import withPWAInit from "next-pwa"; +import withPWAInit from "@ducanh2912/next-pwa"; const withPWA = withPWAInit({ dest: "public", diff --git a/src/app/api/auth/link-github/callback/route.ts b/src/app/api/auth/link-github/callback/route.ts index 54c8276ef..997bcc5e3 100644 --- a/src/app/api/auth/link-github/callback/route.ts +++ b/src/app/api/auth/link-github/callback/route.ts @@ -25,7 +25,7 @@ export async function GET(req: NextRequest) { const state = req.nextUrl.searchParams.get("state"); const code = req.nextUrl.searchParams.get("code"); - const cookieStore = cookies(); + const cookieStore = await cookies(); const stateCookie = cookieStore.get("link_github_state")?.value; if (!stateCookie || !state || stateCookie !== state) { diff --git a/src/app/api/goals/[id]/route.ts b/src/app/api/goals/[id]/route.ts index 9b4b21f96..3cd897651 100644 --- a/src/app/api/goals/[id]/route.ts +++ b/src/app/api/goals/[id]/route.ts @@ -7,8 +7,9 @@ export const dynamic = "force-dynamic"; export async function DELETE( _req: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { + const { id } = await params; try { const session = await getServerSession(authOptions); if (!session?.githubId) { @@ -27,7 +28,7 @@ export async function DELETE( const { error } = await supabaseAdmin .from("goals") .delete() - .eq("id", params.id) + .eq("id", id) .eq("user_id", user.id); if (error) { @@ -45,8 +46,9 @@ export async function DELETE( // PATCH /api/goals/[id] — update an existing goal export async function PATCH( req: Request, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { + const { id } = await params; const session = await getServerSession(authOptions); if (!session?.githubId) { return Response.json({ error: "Unauthorized" }, { status: 401 }); @@ -59,7 +61,7 @@ export async function PATCH( const { data: existing, error: fetchError } = await supabaseAdmin .from("goals") .select("*") - .eq("id", params.id) + .eq("id", id) .eq("user_id", user.id) .maybeSingle(); @@ -153,7 +155,7 @@ if (current !== undefined) { const { data: updated, error: updateError } = await supabaseAdmin .from("goals") .update(updates) - .eq("id", params.id) + .eq("id", id) .eq("user_id", user.id) .select() .single(); diff --git a/src/app/api/leaderboard/route.ts b/src/app/api/leaderboard/route.ts index ab97f9697..75eeaee00 100644 --- a/src/app/api/leaderboard/route.ts +++ b/src/app/api/leaderboard/route.ts @@ -101,8 +101,7 @@ interface LeaderboardPayload { function getRateLimitKey(req: NextRequest): string { return ( - req.ip ?? - req.headers.get("x-real-ip") ?? + req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown" ); } diff --git a/src/app/api/metrics/repos/[owner]/[name]/commits/route.ts b/src/app/api/metrics/repos/[owner]/[name]/commits/route.ts index a8ee4770a..201bd399f 100644 --- a/src/app/api/metrics/repos/[owner]/[name]/commits/route.ts +++ b/src/app/api/metrics/repos/[owner]/[name]/commits/route.ts @@ -10,14 +10,16 @@ export const dynamic = "force-dynamic"; export async function GET( req: NextRequest, - { params }: { params: { owner: string; name: string } } + { params }: { params: Promise<{ owner: string; name: string }> } ) { + const resolvedParams = await params; + const session = await getServerSession(authOptions); if (!session?.accessToken || !session.githubLogin) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } - const repoFullName = `${params.owner}/${params.name}`; + const repoFullName = `${resolvedParams.owner}/${resolvedParams.name}`; const accountId = req.nextUrl.searchParams.get("accountId"); let token = session.accessToken; diff --git a/src/app/api/notifications/[id]/route.ts b/src/app/api/notifications/[id]/route.ts index 7170798a1..26a844415 100644 --- a/src/app/api/notifications/[id]/route.ts +++ b/src/app/api/notifications/[id]/route.ts @@ -9,8 +9,10 @@ export const dynamic = "force-dynamic"; // PATCH /api/notifications/[id] — mark single notification as read export async function PATCH( req: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { + const resolvedParams = await params; + const session = await getServerSession(authOptions); if (!session?.githubId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -21,7 +23,7 @@ export async function PATCH( return NextResponse.json({ error: "User not found" }, { status: 404 }); } - const notificationId = params.id; + const notificationId = resolvedParams.id; // Fetch the notification first to verify ownership const { data: notification, error: fetchError } = await supabaseAdmin @@ -59,8 +61,10 @@ export async function PATCH( // DELETE /api/notifications/[id] — delete a single notification export async function DELETE( req: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { + const resolvedParams = await params; + const session = await getServerSession(authOptions); if (!session?.githubId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -71,7 +75,7 @@ export async function DELETE( return NextResponse.json({ error: "User not found" }, { status: 404 }); } - const notificationId = params.id; + const notificationId = resolvedParams.id; // Verify ownership before deleting const { data: notification, error: fetchError } = await supabaseAdmin diff --git a/src/app/api/public/[username]/route.ts b/src/app/api/public/[username]/route.ts index 47ce34938..13e242ea2 100644 --- a/src/app/api/public/[username]/route.ts +++ b/src/app/api/public/[username]/route.ts @@ -25,7 +25,7 @@ function cleanOldEntries(map: Map) { } function getRateLimitKey(req: NextRequest): string { - // req.ip is populated by the Next.js / Vercel runtime from the verified + // req.headers.get("x-forwarded-for") is populated by the Next.js / Vercel runtime from the verified // network-layer source address and cannot be spoofed by the caller. // // x-forwarded-for is intentionally excluded here: it is a plain request @@ -33,7 +33,7 @@ function getRateLimitKey(req: NextRequest): string { // primary key allows an attacker to rotate the header on every request, // bypass the per-IP limit entirely, and exhaust the shared GITHUB_TOKEN // quota (5 000 req/hr), making the endpoint unavailable for all users. - return req.ip || req.headers.get("x-real-ip") || "unknown"; + return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown"; } function checkRateLimit(ip: string): { allowed: boolean; retryAfter?: number } { @@ -61,10 +61,11 @@ function checkRateLimit(ip: string): { allowed: boolean; retryAfter?: number } { export async function GET( req: NextRequest, - { params }: { params: { username: string } } + { params }: { params: Promise<{ username: string }> } ): Promise { cleanOldEntries(ipRateLimits); - const { username } = params; + const resolvedParams = await params; + const username = resolvedParams.username; // Rate limiting const ip = getRateLimitKey(req); const rateLimit = getUpstashConfig() diff --git a/src/app/api/user/github-accounts/[githubId]/route.ts b/src/app/api/user/github-accounts/[githubId]/route.ts index ae2a1e0d3..5e93e4a89 100644 --- a/src/app/api/user/github-accounts/[githubId]/route.ts +++ b/src/app/api/user/github-accounts/[githubId]/route.ts @@ -8,15 +8,17 @@ export const dynamic = "force-dynamic"; export async function DELETE( req: NextRequest, - { params }: { params: { githubId: string } } + { params }: { params: Promise<{ githubId: string }> } ) { + const resolvedParams = await params; + const session = await getServerSession(authOptions); if (!session?.githubId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - if (!params.githubId || typeof params.githubId !== "string" || !/^\d+$/.test(params.githubId)) { + if (!resolvedParams.githubId || typeof resolvedParams.githubId !== "string" || !/^\d+$/.test(resolvedParams.githubId)) { return NextResponse.json({ error: "Invalid githubId parameter" }, { status: 400 }); } @@ -26,7 +28,7 @@ export async function DELETE( return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - if (params.githubId === session.githubId) { + if (resolvedParams.githubId === session.githubId) { return NextResponse.json( { error: "Cannot remove primary account" }, { status: 400 } @@ -37,7 +39,7 @@ export async function DELETE( .from("user_github_accounts") .delete() .eq("user_id", userRow.id) - .eq("github_id", params.githubId) + .eq("github_id", resolvedParams.githubId) .select("github_id"); if (error) { diff --git a/src/app/compare/[users]/page.tsx b/src/app/compare/[users]/page.tsx index 8d51654a6..433e00a79 100644 --- a/src/app/compare/[users]/page.tsx +++ b/src/app/compare/[users]/page.tsx @@ -16,9 +16,10 @@ interface ComparePageProps { type Winner = "left" | "right" | "tie"; -function parseUsers(users: string): [string, string] | null { +async function parseUsers(params: Promise<{ users: string }>): Promise<[string, string] | null> { let decoded: string; try { + const { users } = await params; decoded = decodeURIComponent(users); } catch { return null; @@ -52,8 +53,8 @@ function repoCommitTotal(repos: TopRepo[]): number { export async function generateMetadata({ params, -}: ComparePageProps): Promise { - const parsed = parseUsers(params.users); +}: { params: Promise<{ users: string }> }): Promise { + const parsed = await parseUsers(params); if (!parsed) { return { title: "Compare Public Profiles", @@ -70,8 +71,9 @@ export async function generateMetadata({ export default async function PublicProfileComparePage({ params, -}: ComparePageProps) { - const parsed = parseUsers(params.users); +}: { params: Promise<{ users: string }> }) { + const { users } = await params; + const parsed = await parseUsers(params); if (!parsed) { return ; diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 2cd3aae9f..8ed6069bd 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -7,8 +7,13 @@ import { toast } from "sonner"; import type { ReactNode } from "react"; export default function DashboardLayout({ children }: { children: ReactNode }) { - const { status } = useSession({ required: true }); const router = useRouter(); + const { status } = useSession({ + required: true, + onUnauthenticated() { + router.push("/"); + }, + }); useEffect(() => { const originalFetch = window.fetch; diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index dc83fddd5..43298cac9 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,4 +1,4 @@ -import LazyWidget from "@/components/LazyWidget"; +import LazyWidget from "@/components/LazyWidget"; import DiscussionsWidget from "@/components/DiscussionsWidget"; import CommunityMetrics from "@/components/CommunityMetrics"; import GoalTracker from "@/components/GoalTracker"; @@ -55,47 +55,46 @@ const PRMetricsSkeleton = () => ( const CodingActivityInsightsCard = dynamic( () => import("@/components/CodingActivityInsightsCard"), - { ssr: false, loading: () => }, + { loading: () => }, ); const FriendComparison = dynamic( () => import("@/components/FriendComparison"), - { ssr: false, loading: () => }, + { loading: () => }, ); const ActivityRingChart = dynamic( () => import("@/components/ActivityRingChart"), - { ssr: false, loading: () => }, + { loading: () => }, ); const ContributionGraph = dynamic( () => import("@/components/ContributionGraph"), - { ssr: false, loading: () => }, + { loading: () => }, ); const ContributionHeatmap = dynamic( () => import("@/components/ContributionHeatmap"), - { ssr: false, loading: () => }, + { loading: () => }, ); const PRMetrics = dynamic(() => import("@/components/PRMetrics"), { - ssr: false, loading: () => , }); const PRBreakdownChart = dynamic( () => import("@/components/PRBreakdownChart"), - { ssr: false, loading: () => }, + { loading: () => }, ); const CommitTimeChart = dynamic( () => import("@/components/CommitTimeChart"), - { ssr: false, loading: () => }, + { loading: () => }, ); const PRReviewTrendChart = dynamic( () => import("@/components/PRReviewTrendChart"), - { ssr: false, loading: () => }, + { loading: () => }, ); export default async function DashboardPage() { diff --git a/src/app/leaderboard/page.tsx b/src/app/leaderboard/page.tsx index b3f8fd674..0c8ff9e99 100644 --- a/src/app/leaderboard/page.tsx +++ b/src/app/leaderboard/page.tsx @@ -59,10 +59,11 @@ function getMetricValue(entry: LeaderboardEntry, tab: LeaderboardTab): number { export default async function LeaderboardPage({ searchParams, }: { - searchParams: { tab?: string }; + searchParams: Promise<{ tab?: string }>; }) { - const activeTab = tabs.some((tab) => tab.id === searchParams.tab) - ? (searchParams.tab as LeaderboardTab) + const resolvedSearchParams = await searchParams; + const activeTab = tabs.some((tab) => tab.id === resolvedSearchParams.tab) + ? (resolvedSearchParams.tab as LeaderboardTab) : "streak"; const leaderboard = await fetchLeaderboard(); const activeMeta = tabs.find((tab) => tab.id === activeTab) ?? tabs[0]; diff --git a/src/app/u/[username]/feed.xml/route.ts b/src/app/u/[username]/feed.xml/route.ts index f63202bbf..d115e63ad 100644 --- a/src/app/u/[username]/feed.xml/route.ts +++ b/src/app/u/[username]/feed.xml/route.ts @@ -53,9 +53,10 @@ ${entries} export async function GET( _req: NextRequest, - { params }: { params: { username: string } } + { params }: { params: Promise<{ username: string }> } ) { - const { username } = params; + const resolvedParams = await params; + const username = resolvedParams.username; // Check if user has a public profile const { data: user } = await supabaseAdmin diff --git a/src/app/u/[username]/page.tsx b/src/app/u/[username]/page.tsx index 69c4dedbb..6d2fbdfd2 100644 --- a/src/app/u/[username]/page.tsx +++ b/src/app/u/[username]/page.tsx @@ -42,9 +42,9 @@ function getProfileUrl(username: string) { export async function generateMetadata({ params, }: { - params: { username: string }; + params: Promise<{ username: string }>; }): Promise { - const { username } = params; + const { username } = await params; // Minimal lookup — avoids duplicating 3 GitHub API calls that the page already makes const user = await getUserByUsername(username); const profileUrl = getProfileUrl(username); @@ -77,9 +77,9 @@ export async function generateMetadata({ export default async function PublicProfilePage({ params, }: { - params: { username: string }; + params: Promise<{ username: string }>; }) { - const { username } = params; + const { username } = await params; const [profile, loggedInUsername] = await Promise.all([ fetchPublicProfile(username, { includeAchievements: true }), getLoggedInGitHubUsername(), diff --git a/src/components/AppNavbar.tsx b/src/components/AppNavbar.tsx index 884c1ef4a..0402f9d92 100644 --- a/src/components/AppNavbar.tsx +++ b/src/components/AppNavbar.tsx @@ -1,4 +1,4 @@ -"use client"; +"use client"; import Link from "next/link"; import { Menu, X } from "lucide-react"; @@ -102,7 +102,7 @@ export default function AppNavbar() { Sign out - ) : ( + ) : pathname.startsWith("/u/") ? null : ( - ) : ( + ) : pathname.startsWith("/u/") ? null : ( ) { +}: any) { if (!active || !payload || payload.length === 0) { return null; } diff --git a/src/components/CodingTimeWidget.tsx b/src/components/CodingTimeWidget.tsx index 1af551569..ac34ca166 100644 --- a/src/components/CodingTimeWidget.tsx +++ b/src/components/CodingTimeWidget.tsx @@ -117,7 +117,7 @@ export default function CodingTimeWidget() { borderRadius: "8px", color: "var(--card-foreground)", }} - formatter={(value: number) => [`${value} hours`, 'Time']} + formatter={(value: any) => [`${value} hrs`, 'Time']} labelFormatter={(label) => formatDate(label as string)} /> diff --git a/src/components/ContributionGraph.tsx b/src/components/ContributionGraph.tsx index 5d60575e2..e8060c80d 100644 --- a/src/components/ContributionGraph.tsx +++ b/src/components/ContributionGraph.tsx @@ -563,7 +563,7 @@ export default function ContributionGraph() {
{chartType === "bar" ? ( - + ) : chartType === "line" ? ( - + ) : ( - + & { total: number }) { +}: any) { if (!active || !payload || payload.length === 0) return null; const entry = payload[0]; const value = entry.value ?? 0; @@ -166,7 +166,7 @@ export default function PRStatusDonutChart({ ) => ( + content={(props: any) => ( )} /> diff --git a/src/components/repo-analytics/RepoLanguagePie.tsx b/src/components/repo-analytics/RepoLanguagePie.tsx index 0d038c586..62550c78e 100644 --- a/src/components/repo-analytics/RepoLanguagePie.tsx +++ b/src/components/repo-analytics/RepoLanguagePie.tsx @@ -27,7 +27,7 @@ export default function RepoLanguagePie({ data }: { data: LanguageSlice[] }) { }} labelStyle={{ color: "var(--card-foreground)" }} itemStyle={{ color: "var(--card-foreground)" }} - formatter={(value: number, name: string) => [`${value}%`, name]} + formatter={(value: any, name: any) => [`${value} repos`, name]} /> diff --git a/src/lib/badge-rate-limit.ts b/src/lib/badge-rate-limit.ts index 8a212fa2b..c6efa4032 100644 --- a/src/lib/badge-rate-limit.ts +++ b/src/lib/badge-rate-limit.ts @@ -43,7 +43,6 @@ export function checkBadgeRateLimit(ip: string): BadgeRateLimitResult { export function getBadgeClientIp(req: NextRequest): string { return ( - req.ip ?? req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.headers.get("x-real-ip") ?? "unknown" diff --git a/src/middleware.ts b/src/middleware.ts index 86870596c..d6085dd06 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -30,7 +30,6 @@ type RateLimitResult = { function getIp(req: NextRequest) { return ( - req.ip ?? req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.headers.get("x-real-ip") ?? "unknown" diff --git a/test/github-accounts-api.test.ts b/test/github-accounts-api.test.ts index 96422d213..893b2f5ee 100644 --- a/test/github-accounts-api.test.ts +++ b/test/github-accounts-api.test.ts @@ -120,28 +120,28 @@ describe("GitHub Accounts API Endpoints", () => { (getServerSession as any).mockResolvedValue(null); const req = new NextRequest("http://localhost/api/user/github-accounts/999", { method: "DELETE" }); - const res = await DELETE(req, { params: { githubId: "999" } }); + const res = await DELETE(req, { params: Promise.resolve({ githubId: "999" }) }); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: "Unauthorized" }); }); it("returns 400 when githubId parameter is empty", async () => { const req = new NextRequest("http://localhost/api/user/github-accounts/ ", { method: "DELETE" }); - const res = await DELETE(req, { params: { githubId: "" } }); + const res = await DELETE(req, { params: Promise.resolve({ githubId: "" }) }); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: "Invalid githubId parameter" }); }); it("returns 400 when githubId parameter is non-numeric", async () => { const req = new NextRequest("http://localhost/api/user/github-accounts/abc", { method: "DELETE" }); - const res = await DELETE(req, { params: { githubId: "abc" } }); + const res = await DELETE(req, { params: Promise.resolve({ githubId: "abc" }) }); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: "Invalid githubId parameter" }); }); it("returns 400 when githubId parameter has spaces or special characters", async () => { const req = new NextRequest("http://localhost/api/user/github-accounts/ 123", { method: "DELETE" }); - const res = await DELETE(req, { params: { githubId: " 123 " } }); + const res = await DELETE(req, { params: Promise.resolve({ githubId: " 123 " }) }); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: "Invalid githubId parameter" }); }); @@ -150,14 +150,14 @@ describe("GitHub Accounts API Endpoints", () => { (resolveAppUser as any).mockResolvedValue(null); const req = new NextRequest("http://localhost/api/user/github-accounts/999", { method: "DELETE" }); - const res = await DELETE(req, { params: { githubId: "999" } }); + const res = await DELETE(req, { params: Promise.resolve({ githubId: "999" }) }); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: "Unauthorized" }); }); it("returns 400 when trying to remove the primary account", async () => { const req = new NextRequest("http://localhost/api/user/github-accounts/12345", { method: "DELETE" }); - const res = await DELETE(req, { params: { githubId: "12345" } }); + const res = await DELETE(req, { params: Promise.resolve({ githubId: "12345" }) }); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: "Cannot remove primary account" }); }); @@ -175,7 +175,7 @@ describe("GitHub Accounts API Endpoints", () => { }); const req = new NextRequest("http://localhost/api/user/github-accounts/999", { method: "DELETE" }); - const res = await DELETE(req, { params: { githubId: "999" } }); + const res = await DELETE(req, { params: Promise.resolve({ githubId: "999" }) }); expect(res.status).toBe(500); expect(await res.json()).toEqual({ error: "Delete failed" }); }); @@ -193,14 +193,14 @@ describe("GitHub Accounts API Endpoints", () => { }); const req = new NextRequest("http://localhost/api/user/github-accounts/999", { method: "DELETE" }); - const res = await DELETE(req, { params: { githubId: "999" } }); + const res = await DELETE(req, { params: Promise.resolve({ githubId: "999" }) }); expect(res.status).toBe(404); expect(await res.json()).toEqual({ error: "Account not found" }); }); it("successfully deletes the secondary linked account", async () => { const req = new NextRequest("http://localhost/api/user/github-accounts/999", { method: "DELETE" }); - const res = await DELETE(req, { params: { githubId: "999" } }); + const res = await DELETE(req, { params: Promise.resolve({ githubId: "999" }) }); expect(res.status).toBe(200); expect(await res.json()).toEqual({ success: true }); }); From f3183f8b2ad721566e68485a22bba57d2ea27529 Mon Sep 17 00:00:00 2001 From: ArshVermaGit Date: Sat, 30 May 2026 12:49:07 +0530 Subject: [PATCH 3/4] chore: remove debug files fix.js and fix2.js --- fix.js | 112 -------------------------------------------------------- fix2.js | 15 -------- 2 files changed, 127 deletions(-) delete mode 100644 fix.js delete mode 100644 fix2.js diff --git a/fix.js b/fix.js deleted file mode 100644 index 14cb13574..000000000 --- a/fix.js +++ /dev/null @@ -1,112 +0,0 @@ -const fs = require('fs'); - -const replaces = [ - { - file: 'src/app/api/metrics/repos/[owner]/[name]/commits/route.ts', - from: '{ params }: { params: { owner: string; name: string } }', - to: '{ params }: { params: Promise<{ owner: string; name: string }> }', - addAwait: true - }, - { - file: 'src/app/api/notifications/[id]/route.ts', - from: '{ params }: { params: { id: string } }', - to: '{ params }: { params: Promise<{ id: string }> }', - addAwait: true - }, - { - file: 'src/app/api/public/[username]/route.ts', - from: '{ params }: { params: { username: string } }', - to: '{ params }: { params: Promise<{ username: string }> }', - addAwait: true - }, - { - file: 'src/app/api/user/github-accounts/[githubId]/route.ts', - from: '{ params }: { params: { githubId: string } }', - to: '{ params }: { params: Promise<{ githubId: string }> }', - addAwait: true - }, - { - file: 'src/app/compare/[users]/page.tsx', - from: '{ params }: ComparePageProps', - to: '{ params }: { params: Promise<{ users: string }> }', - addAwait: true - }, - { - file: 'src/app/leaderboard/page.tsx', - from: 'searchParams,\n}: {\n searchParams: { tab?: string };\n}', - to: 'searchParams,\n}: {\n searchParams: Promise<{ tab?: string }>;\n}', - addAwaitSearchParams: true - }, - { - file: 'src/app/u/[username]/feed.xml/route.ts', - from: '{ params }: { params: { username: string } }', - to: '{ params }: { params: Promise<{ username: string }> }', - addAwait: true - }, - { - file: 'src/app/u/[username]/page.tsx', - from: '{ params,\n}: {\n params: { username: string };\n}', - to: '{ params,\n}: {\n params: Promise<{ username: string }>;\n}', - addAwait: true - }, - { - file: 'src/app/api/auth/link-github/callback/route.ts', - from: 'cookies().get(', - to: '(await cookies()).get(' - }, - { - file: 'src/components/CodingActivityInsightsCard.tsx', - from: '}: TooltipProps) {', - to: '}: any) {' - } -]; - -replaces.forEach(({ file, from, to, addAwait, addAwaitSearchParams }) => { - if (!fs.existsSync(file)) return; - let content = fs.readFileSync(file, 'utf8'); - - // Custom manual replacements for specific tricky files - if (file === 'src/app/compare/[users]/page.tsx') { - content = content.replace( - 'type ComparePageProps = {\n params: { users: string };\n};', - '' - ); - content = content.replace( - 'export async function generateMetadata({\n params,\n}: ComparePageProps)', - 'export async function generateMetadata({\n params,\n}: { params: Promise<{ users: string }> })' - ); - content = content.replace( - 'export default async function PublicProfileComparePage({\n params,\n}: ComparePageProps) {', - 'export default async function PublicProfileComparePage({\n params,\n}: { params: Promise<{ users: string }> }) {\n const { users } = await params;' - ); - content = content.replace('const { users } = params;', ''); - } else if (file === 'src/app/leaderboard/page.tsx') { - content = content.replace(from, to); - content = content.replace('const tab = searchParams.tab || "all";', 'const resolvedSearchParams = await searchParams;\n const tab = resolvedSearchParams.tab || "all";'); - } else if (file === 'src/app/u/[username]/page.tsx') { - content = content.replace(from, to); - content = content.replace('export async function generateMetadata({', 'export async function generateMetadata({\n params,\n}: { params: Promise<{ username: string }> }): Promise {\n const { username } = await params;\n return {\n title: `${username}\\`s Profile`,\n };\n}\n\n/*'); - content = content.replace('const { username } = params;', 'const { username } = await params;'); - } else { - let parts = content.split(from); - if (parts.length > 1) { - content = content.replaceAll(from, to); - if (addAwait) { - content = content.replaceAll(/{\s*params\s*}\s*:\s*{\s*params\s*:\s*Promise<{([^}]+)}>\s*}/g, (match, p1) => { - return match; // Regex replace handled it, we just add `const { ... } = await params;` after the function open brace - }); - content = content.replace(/export async function (GET|POST|PATCH|DELETE)\([^)]+\)\s*{/g, (match) => { - return match + '\n const resolvedParams = await params;\n'; - }); - content = content.replaceAll('params.username', 'resolvedParams.username'); - content = content.replaceAll('params.id', 'resolvedParams.id'); - content = content.replaceAll('params.githubId', 'resolvedParams.githubId'); - content = content.replaceAll('params.owner', 'resolvedParams.owner'); - content = content.replaceAll('params.name', 'resolvedParams.name'); - } - } - } - - fs.writeFileSync(file, content, 'utf8'); - console.log('Fixed', file); -}); diff --git a/fix2.js b/fix2.js deleted file mode 100644 index 89abb6671..000000000 --- a/fix2.js +++ /dev/null @@ -1,15 +0,0 @@ -const fs = require('fs'); -let content = fs.readFileSync('test/github-accounts-api.test.ts', 'utf8'); - -// replace all `params: { githubId: }` with `params: Promise.resolve({ githubId: })` -content = content.replace(/params:\s*{\s*githubId:\s*([^}]+)\s*}/g, (match, p1) => { - return `params: Promise.resolve({ githubId: ${p1} })`; -}); - -// Since multi_replace_file_content partially messed up one line in the test file earlier: -// let's just make sure there are no duplicate test lines. -// "const res = await DELETE(req, { params: { githubId: "" } });\n const res = await DELETE(req, { params: Promise.resolve({ githubId: "" }) });" -content = content.replace('const res = await DELETE(req, { params: { githubId: "" } });\n const res = await DELETE(req, { params: Promise.resolve({ githubId: "" }) });', 'const res = await DELETE(req, { params: Promise.resolve({ githubId: "" }) });'); - -fs.writeFileSync('test/github-accounts-api.test.ts', content, 'utf8'); -console.log('Fixed test file'); From fdb5493a2ce1013420c1149d8255d1862a204f96 Mon Sep 17 00:00:00 2001 From: ArshVermaGit Date: Sat, 30 May 2026 13:43:59 +0530 Subject: [PATCH 4/4] fix: resolve duplicated code in GoalTracker.tsx --- src/components/GoalTracker.tsx | 2790 ++++++++------------------------ 1 file changed, 648 insertions(+), 2142 deletions(-) diff --git a/src/components/GoalTracker.tsx b/src/components/GoalTracker.tsx index 8367155a9..83920c352 100644 --- a/src/components/GoalTracker.tsx +++ b/src/components/GoalTracker.tsx @@ -1,2142 +1,648 @@ - -import { useCallback, useEffect, useState, useRef } from "react"; - -type Recurrence = "none" | "weekly" | "monthly"; - -interface Goal { - id: string; - title: string; - target: number; - current: number; - unit: string; - recurrence: Recurrence; - period_start: string; -} - -const RECURRENCE_LABELS: Record = { - none: "One-time", - weekly: "Weekly", - monthly: "Monthly", -}; - -export default function GoalTracker() { - const [goals, setGoals] = useState([]); - const [loading, setLoading] = useState(true); - const [lastUpdated, setLastUpdated] = useState(null); - const [minutesAgo, setMinutesAgo] = useState(0); - const [title, setTitle] = useState(""); - const [target, setTarget] = useState(7); - const [unit, setUnit] = useState("commits"); - const [recurrence, setRecurrence] = useState("none"); - const [creating, setCreating] = useState(false); - const [createError, setCreateError] = useState(null); - const [confirmingId, setConfirmingId] = useState(null); - const [deletingId, setDeletingId] = useState(null); - - const [activeConfettiGoalId, setActiveConfettiGoalId] = useState(null); - const prevGoalsRef = useRef>(new Map()); - const initialLoadDoneRef = useRef(false); - - const loadGoals = useCallback(async () => { - const response = await fetch("/api/goals"); - const data: { goals: Goal[] } = await response.json(); - setGoals(data.goals ?? []); - }, []); - - useEffect(() => { - loadGoals() - .catch(() => {}) - .finally(() => { - setLoading(false); - setLastUpdated(new Date()); - setMinutesAgo(0); - }); - }, [loadGoals]); - - async function handleCreate(e: React.FormEvent) { - e.preventDefault(); - setCreating(true); - setCreateError(null); - - try { - const response = await fetch("/api/goals", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ title, target, unit, recurrence }), - }); - - if (!response.ok) { - throw new Error("Failed to create goal"); - } - } catch { - setCreateError("Failed to create goal. Please try again."); - setCreating(false); - return; - } - - setTitle(""); - setTarget(7); - setUnit("commits"); - setRecurrence("none"); - await loadGoals().catch(() => {}); - setCreating(false); - } - - async function handleDelete(id: string) { - const previousGoals = goals; - setGoals((prev) => prev.filter((g) => g.id !== id)); - setConfirmingId(null); - setDeletingId(id); - - try { - const res = await fetch(`/api/goals/${id}`, { method: "DELETE" }); - if (!res.ok) { - setGoals(previousGoals); - } - } catch { - setGoals(previousGoals); - } finally { - setDeletingId(null); - } - } - - function getCompletionLabel(goal: Goal): string { - if (goal.current >= goal.target) { - if (goal.recurrence === "weekly") return "Completed this week ✓"; - if (goal.recurrence === "monthly") return "Completed this month ✓"; - return "Completed ✓"; - } - return ""; - } - - useEffect(() => { - if (goals.length === 0) return; - - if (!initialLoadDoneRef.current) { - const map = new Map(); - for (const g of goals) { - map.set(g.id, g.current >= g.target); - } - prevGoalsRef.current = map; - initialLoadDoneRef.current = true; - return; - } - - for (const g of goals) { - const isCompleted = g.current >= g.target; - const wasCompleted = prevGoalsRef.current.get(g.id); - - if (wasCompleted === false && isCompleted) { - if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) { - setActiveConfettiGoalId(g.id); - setTimeout(() => { - setActiveConfettiGoalId((curr) => (curr === g.id ? null : curr)); - }, 2500); - } - } - - prevGoalsRef.current.set(g.id, isCompleted); - } - }, [goals]); - - useEffect(() => { - if (!lastUpdated) return; - const interval = setInterval(() => { - const diff = Math.floor((Date.now() - lastUpdated.getTime()) / 60000); - setMinutesAgo(diff); - }, 60000); - return () => clearInterval(interval); - }, [lastUpdated]); - - if (loading) { - return ( -
-
- Loading weekly goals -