feat: integrate Socket.IO for real-time communication; add socket connection management and token refresh handling
This commit is contained in:
21
bun.lock
21
bun.lock
@@ -32,6 +32,7 @@
|
|||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router": "^7.7.1",
|
"react-router": "^7.7.1",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
@@ -312,6 +313,8 @@
|
|||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.1", "", { "os": "win32", "cpu": "x64" }, "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.1", "", { "os": "win32", "cpu": "x64" }, "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA=="],
|
||||||
|
|
||||||
|
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||||
|
|
||||||
"@swc/core": ["@swc/core@1.13.2", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.2", "@swc/core-darwin-x64": "1.13.2", "@swc/core-linux-arm-gnueabihf": "1.13.2", "@swc/core-linux-arm64-gnu": "1.13.2", "@swc/core-linux-arm64-musl": "1.13.2", "@swc/core-linux-x64-gnu": "1.13.2", "@swc/core-linux-x64-musl": "1.13.2", "@swc/core-win32-arm64-msvc": "1.13.2", "@swc/core-win32-ia32-msvc": "1.13.2", "@swc/core-win32-x64-msvc": "1.13.2" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg=="],
|
"@swc/core": ["@swc/core@1.13.2", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.2", "@swc/core-darwin-x64": "1.13.2", "@swc/core-linux-arm-gnueabihf": "1.13.2", "@swc/core-linux-arm64-gnu": "1.13.2", "@swc/core-linux-arm64-musl": "1.13.2", "@swc/core-linux-x64-gnu": "1.13.2", "@swc/core-linux-x64-musl": "1.13.2", "@swc/core-win32-arm64-msvc": "1.13.2", "@swc/core-win32-ia32-msvc": "1.13.2", "@swc/core-win32-x64-msvc": "1.13.2" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg=="],
|
||||||
|
|
||||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw=="],
|
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw=="],
|
||||||
@@ -500,6 +503,10 @@
|
|||||||
|
|
||||||
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
||||||
|
|
||||||
|
"engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="],
|
||||||
|
|
||||||
|
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
|
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.25.8", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="],
|
"esbuild": ["esbuild@0.25.8", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="],
|
||||||
@@ -732,6 +739,10 @@
|
|||||||
|
|
||||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"socket.io-client": ["socket.io-client@4.8.1", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ=="],
|
||||||
|
|
||||||
|
"socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="],
|
||||||
|
|
||||||
"sonner": ["sonner@2.0.6", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q=="],
|
"sonner": ["sonner@2.0.6", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
@@ -784,6 +795,10 @@
|
|||||||
|
|
||||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
|
"ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||||
|
|
||||||
|
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||||
|
|
||||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
@@ -810,12 +825,18 @@
|
|||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
|
"engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
|
"socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||||
|
|
||||||
|
"socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router": "^7.7.1",
|
"react-router": "^7.7.1",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.11"
|
"tailwindcss": "^4.1.11"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Routes, Route, Navigate } from 'react-router'
|
import { Routes, Route, Navigate } from 'react-router'
|
||||||
import { ThemeProvider } from './components/ThemeProvider'
|
import { ThemeProvider } from './components/ThemeProvider'
|
||||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||||
|
import { SocketProvider } from './contexts/SocketContext'
|
||||||
import { LoginPage } from './pages/LoginPage'
|
import { LoginPage } from './pages/LoginPage'
|
||||||
import { RegisterPage } from './pages/RegisterPage'
|
import { RegisterPage } from './pages/RegisterPage'
|
||||||
import { AuthCallbackPage } from './pages/AuthCallbackPage'
|
import { AuthCallbackPage } from './pages/AuthCallbackPage'
|
||||||
@@ -42,8 +43,10 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<AppRoutes />
|
<SocketProvider>
|
||||||
<Toaster />
|
<AppRoutes />
|
||||||
|
<Toaster />
|
||||||
|
</SocketProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
50
src/components/SocketStatus.tsx
Normal file
50
src/components/SocketStatus.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { useSocket } from '../contexts/SocketContext'
|
||||||
|
import { Badge } from './ui/badge'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card'
|
||||||
|
|
||||||
|
export function SocketStatus() {
|
||||||
|
const { isConnected, connectionError, isReconnecting } = useSocket()
|
||||||
|
|
||||||
|
const getStatusBadge = () => {
|
||||||
|
if (isReconnecting) {
|
||||||
|
return <Badge variant="secondary">Reconnecting...</Badge>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Badge variant={isConnected ? 'default' : 'destructive'}>
|
||||||
|
{isConnected ? 'Connected' : 'Disconnected'}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusMessage = () => {
|
||||||
|
if (isReconnecting) {
|
||||||
|
return <div className="text-muted-foreground text-sm">Reconnecting with refreshed token...</div>
|
||||||
|
}
|
||||||
|
if (connectionError) {
|
||||||
|
return <div className="text-destructive text-sm">{connectionError}</div>
|
||||||
|
}
|
||||||
|
if (isConnected) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-muted-foreground text-sm">Ready for real-time communication</div>
|
||||||
|
<div className="text-xs text-muted-foreground">🔄 Proactive token refresh active</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
WebSocket Status
|
||||||
|
{getStatusBadge()}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{getStatusMessage()}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import type { AuthContextType, User, LoginRequest, RegisterRequest } from '@/types/auth'
|
import type { AuthContextType, User, LoginRequest, RegisterRequest } from '@/types/auth'
|
||||||
|
import { authEvents, AUTH_EVENTS } from '@/lib/events'
|
||||||
|
import { tokenRefreshManager } from '@/lib/token-refresh-manager'
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | null>(null)
|
const AuthContext = createContext<AuthContextType | null>(null)
|
||||||
|
|
||||||
@@ -26,8 +28,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
// Try to get user info using cookies
|
// Try to get user info using cookies
|
||||||
const user = await api.auth.getMe()
|
const user = await api.auth.getMe()
|
||||||
setUser(user)
|
setUser(user)
|
||||||
|
// Token refresh manager will be started by the user effect below
|
||||||
} catch {
|
} catch {
|
||||||
// User is not authenticated - this is normal for logged out users
|
// User is not authenticated - this is normal for logged out users
|
||||||
|
// Token refresh manager will be stopped by the user effect below
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -35,19 +39,31 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
initAuth()
|
initAuth()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Start/stop token refresh manager based on user authentication
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
tokenRefreshManager.start()
|
||||||
|
} else {
|
||||||
|
tokenRefreshManager.stop()
|
||||||
|
}
|
||||||
|
}, [user])
|
||||||
|
|
||||||
const login = async (credentials: LoginRequest) => {
|
const login = async (credentials: LoginRequest) => {
|
||||||
const user = await api.auth.login(credentials)
|
const user = await api.auth.login(credentials)
|
||||||
setUser(user)
|
setUser(user)
|
||||||
|
authEvents.emit(AUTH_EVENTS.LOGIN_SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
const register = async (data: RegisterRequest) => {
|
const register = async (data: RegisterRequest) => {
|
||||||
const user = await api.auth.register(data)
|
const user = await api.auth.register(data)
|
||||||
setUser(user)
|
setUser(user)
|
||||||
|
authEvents.emit(AUTH_EVENTS.LOGIN_SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
await api.auth.logout()
|
await api.auth.logout()
|
||||||
setUser(null)
|
setUser(null)
|
||||||
|
authEvents.emit(AUTH_EVENTS.LOGOUT)
|
||||||
}
|
}
|
||||||
|
|
||||||
const value: AuthContextType = {
|
const value: AuthContextType = {
|
||||||
|
|||||||
126
src/contexts/SocketContext.tsx
Normal file
126
src/contexts/SocketContext.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react'
|
||||||
|
import { io, Socket } from 'socket.io-client'
|
||||||
|
import { useAuth } from './AuthContext'
|
||||||
|
import { authEvents, AUTH_EVENTS } from '../lib/events'
|
||||||
|
|
||||||
|
interface SocketContextType {
|
||||||
|
socket: Socket | null
|
||||||
|
isConnected: boolean
|
||||||
|
connectionError: string | null
|
||||||
|
isReconnecting: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const SocketContext = createContext<SocketContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
interface SocketProviderProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SocketProvider({ children }: SocketProviderProps) {
|
||||||
|
const { user, loading } = useAuth()
|
||||||
|
const [socket, setSocket] = useState<Socket | null>(null)
|
||||||
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
|
const [connectionError, setConnectionError] = useState<string | null>(null)
|
||||||
|
const [isReconnecting, setIsReconnecting] = useState(false)
|
||||||
|
|
||||||
|
const createSocket = useCallback(() => {
|
||||||
|
if (!user) return null
|
||||||
|
|
||||||
|
const newSocket = io('http://localhost:8000', {
|
||||||
|
withCredentials: true,
|
||||||
|
transports: ['polling', 'websocket'],
|
||||||
|
timeout: 20000,
|
||||||
|
forceNew: true,
|
||||||
|
autoConnect: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
newSocket.on('connect', () => {
|
||||||
|
setIsConnected(true)
|
||||||
|
setConnectionError(null)
|
||||||
|
setIsReconnecting(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
newSocket.on('disconnect', () => {
|
||||||
|
setIsConnected(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
newSocket.on('connect_error', (error) => {
|
||||||
|
setConnectionError(`Connection failed: ${error.message}`)
|
||||||
|
setIsConnected(false)
|
||||||
|
setIsReconnecting(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
return newSocket
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
// Handle token refresh - reconnect socket with new token
|
||||||
|
const handleTokenRefresh = useCallback(() => {
|
||||||
|
if (!user || !socket) return
|
||||||
|
|
||||||
|
setIsReconnecting(true)
|
||||||
|
|
||||||
|
// Disconnect current socket
|
||||||
|
socket.disconnect()
|
||||||
|
|
||||||
|
// Create new socket with fresh token
|
||||||
|
const newSocket = createSocket()
|
||||||
|
if (newSocket) {
|
||||||
|
setSocket(newSocket)
|
||||||
|
}
|
||||||
|
}, [user, socket, createSocket])
|
||||||
|
|
||||||
|
// Listen for token refresh events
|
||||||
|
useEffect(() => {
|
||||||
|
authEvents.on(AUTH_EVENTS.TOKEN_REFRESHED, handleTokenRefresh)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
authEvents.off(AUTH_EVENTS.TOKEN_REFRESHED, handleTokenRefresh)
|
||||||
|
}
|
||||||
|
}, [handleTokenRefresh])
|
||||||
|
|
||||||
|
// Initial socket connection
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) return
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect()
|
||||||
|
setSocket(null)
|
||||||
|
setIsConnected(false)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSocket = createSocket()
|
||||||
|
if (newSocket) {
|
||||||
|
setSocket(newSocket)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (newSocket) {
|
||||||
|
newSocket.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [loading, user, createSocket])
|
||||||
|
|
||||||
|
const value: SocketContextType = {
|
||||||
|
socket,
|
||||||
|
isConnected,
|
||||||
|
connectionError,
|
||||||
|
isReconnecting,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SocketContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</SocketContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSocket() {
|
||||||
|
const context = useContext(SocketContext)
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useSocket must be used within a SocketProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { API_CONFIG } from './config'
|
import { API_CONFIG } from './config'
|
||||||
import { createApiError, NetworkError, TimeoutError } from './errors'
|
import { createApiError, NetworkError, TimeoutError } from './errors'
|
||||||
import type { ApiClient, ApiRequestConfig, HttpMethod } from './types'
|
import type { ApiClient, ApiRequestConfig, HttpMethod } from './types'
|
||||||
|
import { authEvents, AUTH_EVENTS } from '../events'
|
||||||
|
|
||||||
export class BaseApiClient implements ApiClient {
|
export class BaseApiClient implements ApiClient {
|
||||||
private refreshPromise: Promise<void> | null = null
|
private refreshPromise: Promise<void> | null = null
|
||||||
@@ -147,6 +148,9 @@ export class BaseApiClient implements ApiClient {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw createApiError(response, await this.safeParseJSON(response))
|
throw createApiError(response, await this.safeParseJSON(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit token refresh event for socket reconnection (reactive refresh)
|
||||||
|
authEvents.emit(AUTH_EVENTS.TOKEN_REFRESHED)
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAuthenticationFailure(): void {
|
private handleAuthenticationFailure(): void {
|
||||||
|
|||||||
43
src/lib/events.ts
Normal file
43
src/lib/events.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Simple event emitter for cross-component communication
|
||||||
|
*/
|
||||||
|
|
||||||
|
type EventHandler = (...args: any[]) => void
|
||||||
|
|
||||||
|
class EventEmitter {
|
||||||
|
private events: Map<string, EventHandler[]> = new Map()
|
||||||
|
|
||||||
|
on(event: string, handler: EventHandler): void {
|
||||||
|
if (!this.events.has(event)) {
|
||||||
|
this.events.set(event, [])
|
||||||
|
}
|
||||||
|
this.events.get(event)!.push(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: string, handler: EventHandler): void {
|
||||||
|
const handlers = this.events.get(event)
|
||||||
|
if (handlers) {
|
||||||
|
const index = handlers.indexOf(handler)
|
||||||
|
if (index > -1) {
|
||||||
|
handlers.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: string, ...args: any[]): void {
|
||||||
|
const handlers = this.events.get(event)
|
||||||
|
if (handlers) {
|
||||||
|
handlers.forEach(handler => handler(...args))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authEvents = new EventEmitter()
|
||||||
|
|
||||||
|
// Auth event types
|
||||||
|
export const AUTH_EVENTS = {
|
||||||
|
TOKEN_REFRESHED: 'token_refreshed',
|
||||||
|
TOKEN_EXPIRED: 'token_expired',
|
||||||
|
LOGIN_SUCCESS: 'login_success',
|
||||||
|
LOGOUT: 'logout',
|
||||||
|
} as const
|
||||||
149
src/lib/token-refresh-manager.ts
Normal file
149
src/lib/token-refresh-manager.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* Token refresh manager for proactive token refresh
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { authEvents, AUTH_EVENTS } from './events'
|
||||||
|
import { api } from './api'
|
||||||
|
|
||||||
|
export class TokenRefreshManager {
|
||||||
|
private refreshTimer: NodeJS.Timeout | null = null
|
||||||
|
private refreshInterval = 10 * 60 * 1000 // Refresh every 10 minutes (tokens expire in 15 min)
|
||||||
|
private isRefreshing = false
|
||||||
|
private isEnabled = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start proactive token refresh monitoring
|
||||||
|
*/
|
||||||
|
start(): void {
|
||||||
|
// Prevent multiple starts
|
||||||
|
if (this.isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isEnabled = true
|
||||||
|
this.scheduleNextRefresh()
|
||||||
|
|
||||||
|
// Listen for visibility changes to handle tab switching
|
||||||
|
document.addEventListener('visibilitychange', this.handleVisibilityChange)
|
||||||
|
|
||||||
|
// Listen for successful auth events to reschedule
|
||||||
|
authEvents.on(AUTH_EVENTS.LOGIN_SUCCESS, this.handleAuthSuccess)
|
||||||
|
authEvents.on(AUTH_EVENTS.TOKEN_REFRESHED, this.handleTokenRefreshed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop proactive token refresh monitoring
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isEnabled = false
|
||||||
|
this.clearRefreshTimer()
|
||||||
|
|
||||||
|
document.removeEventListener('visibilitychange', this.handleVisibilityChange)
|
||||||
|
authEvents.off(AUTH_EVENTS.LOGIN_SUCCESS, this.handleAuthSuccess)
|
||||||
|
authEvents.off(AUTH_EVENTS.TOKEN_REFRESHED, this.handleTokenRefreshed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule the next token refresh using fixed interval
|
||||||
|
*/
|
||||||
|
private scheduleNextRefresh = (): void => {
|
||||||
|
if (!this.isEnabled) return
|
||||||
|
|
||||||
|
this.clearRefreshTimer()
|
||||||
|
|
||||||
|
this.refreshTimer = setTimeout(() => {
|
||||||
|
this.performProactiveRefresh()
|
||||||
|
}, this.refreshInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform proactive token refresh
|
||||||
|
*/
|
||||||
|
private performProactiveRefresh = async (): Promise<void> => {
|
||||||
|
if (this.isRefreshing || !this.isEnabled) return
|
||||||
|
|
||||||
|
this.isRefreshing = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the API client's refresh method directly
|
||||||
|
await api.auth.refreshToken()
|
||||||
|
|
||||||
|
authEvents.emit(AUTH_EVENTS.TOKEN_REFRESHED)
|
||||||
|
|
||||||
|
// Schedule next refresh immediately since we just completed one
|
||||||
|
this.scheduleNextRefresh()
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
// If refresh fails, try again in 1 minute
|
||||||
|
this.refreshTimer = setTimeout(() => {
|
||||||
|
this.performProactiveRefresh()
|
||||||
|
}, 60 * 1000)
|
||||||
|
} finally {
|
||||||
|
this.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle tab visibility changes
|
||||||
|
*/
|
||||||
|
private handleVisibilityChange = (): void => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
// Tab became active - check if we need to refresh immediately
|
||||||
|
this.checkTokenFreshnessOnActivation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check token freshness when tab becomes active
|
||||||
|
*/
|
||||||
|
private checkTokenFreshnessOnActivation = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// Try to make an API call to see if token is still valid
|
||||||
|
await api.auth.getMe()
|
||||||
|
|
||||||
|
// Token is still valid, reschedule based on remaining time
|
||||||
|
this.scheduleNextRefresh()
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorObj = error as { status?: number; message?: string }
|
||||||
|
if (errorObj?.status === 401 || errorObj?.message?.includes('401')) {
|
||||||
|
// Token is expired, trigger refresh immediately
|
||||||
|
this.performProactiveRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle successful auth events
|
||||||
|
*/
|
||||||
|
private handleAuthSuccess = (): void => {
|
||||||
|
this.scheduleNextRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle token refresh events
|
||||||
|
*/
|
||||||
|
private handleTokenRefreshed = (): void => {
|
||||||
|
// Only reschedule if this wasn't our own refresh to avoid double scheduling
|
||||||
|
if (!this.isRefreshing) {
|
||||||
|
this.scheduleNextRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the current refresh timer
|
||||||
|
*/
|
||||||
|
private clearRefreshTimer(): void {
|
||||||
|
if (this.refreshTimer) {
|
||||||
|
clearTimeout(this.refreshTimer)
|
||||||
|
this.refreshTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global token refresh manager instance
|
||||||
|
export const tokenRefreshManager = new TokenRefreshManager()
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { ModeToggle } from '../components/ModeToggle'
|
import { ModeToggle } from '../components/ModeToggle'
|
||||||
|
import { SocketStatus } from '../components/SocketStatus'
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
@@ -28,8 +29,11 @@ export function DashboardPage() {
|
|||||||
|
|
||||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
<div className="px-4 py-6 sm:px-0">
|
<div className="px-4 py-6 sm:px-0">
|
||||||
<div className="border-4 border-dashed border-gray-200 dark:border-gray-700 rounded-lg h-96 flex items-center justify-center">
|
<div className="space-y-6">
|
||||||
<p className="text-gray-500 dark:text-gray-400">Dashboard content coming soon...</p>
|
<SocketStatus />
|
||||||
|
<div className="border-4 border-dashed border-gray-200 dark:border-gray-700 rounded-lg h-96 flex items-center justify-center">
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">Dashboard content coming soon...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user