diff --git a/bun.lock b/bun.lock index 311f454..45c44d9 100644 --- a/bun.lock +++ b/bun.lock @@ -32,6 +32,7 @@ "react-dom": "^19.1.0", "react-router": "^7.7.1", "recharts": "2.15.4", + "socket.io-client": "^4.8.1", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "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=="], + "@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-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=="], + "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=="], "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=="], + "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=="], "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=="], + "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=="], "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=="], + "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=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "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=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], diff --git a/package.json b/package.json index b394c8b..2823411 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react-dom": "^19.1.0", "react-router": "^7.7.1", "recharts": "2.15.4", + "socket.io-client": "^4.8.1", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11" diff --git a/src/App.tsx b/src/App.tsx index d40b79f..f8dab89 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { Routes, Route, Navigate } from 'react-router' import { ThemeProvider } from './components/ThemeProvider' import { AuthProvider, useAuth } from './contexts/AuthContext' +import { SocketProvider } from './contexts/SocketContext' import { LoginPage } from './pages/LoginPage' import { RegisterPage } from './pages/RegisterPage' import { AuthCallbackPage } from './pages/AuthCallbackPage' @@ -42,8 +43,10 @@ function App() { return ( - - + + + + ) diff --git a/src/components/SocketStatus.tsx b/src/components/SocketStatus.tsx new file mode 100644 index 0000000..734c485 --- /dev/null +++ b/src/components/SocketStatus.tsx @@ -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 Reconnecting... + } + return ( + + {isConnected ? 'Connected' : 'Disconnected'} + + ) + } + + const getStatusMessage = () => { + if (isReconnecting) { + return
Reconnecting with refreshed token...
+ } + if (connectionError) { + return
{connectionError}
+ } + if (isConnected) { + return ( +
+
Ready for real-time communication
+
🔄 Proactive token refresh active
+
+ ) + } + return null + } + + return ( + + + + WebSocket Status + {getStatusBadge()} + + + + {getStatusMessage()} + + + ) +} \ No newline at end of file diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index e797b6b..0320407 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,6 +1,8 @@ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' import { api } from '@/lib/api' 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(null) @@ -26,8 +28,10 @@ export function AuthProvider({ children }: AuthProviderProps) { // Try to get user info using cookies const user = await api.auth.getMe() setUser(user) + // Token refresh manager will be started by the user effect below } catch { // User is not authenticated - this is normal for logged out users + // Token refresh manager will be stopped by the user effect below } setLoading(false) } @@ -35,19 +39,31 @@ export function AuthProvider({ children }: AuthProviderProps) { 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 user = await api.auth.login(credentials) setUser(user) + authEvents.emit(AUTH_EVENTS.LOGIN_SUCCESS) } const register = async (data: RegisterRequest) => { const user = await api.auth.register(data) setUser(user) + authEvents.emit(AUTH_EVENTS.LOGIN_SUCCESS) } const logout = async () => { await api.auth.logout() setUser(null) + authEvents.emit(AUTH_EVENTS.LOGOUT) } const value: AuthContextType = { diff --git a/src/contexts/SocketContext.tsx b/src/contexts/SocketContext.tsx new file mode 100644 index 0000000..25bcb46 --- /dev/null +++ b/src/contexts/SocketContext.tsx @@ -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(undefined) + +interface SocketProviderProps { + children: React.ReactNode +} + +export function SocketProvider({ children }: SocketProviderProps) { + const { user, loading } = useAuth() + const [socket, setSocket] = useState(null) + const [isConnected, setIsConnected] = useState(false) + const [connectionError, setConnectionError] = useState(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 ( + + {children} + + ) +} + +export function useSocket() { + const context = useContext(SocketContext) + if (context === undefined) { + throw new Error('useSocket must be used within a SocketProvider') + } + return context +} \ No newline at end of file diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index c07356e..0808717 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -1,6 +1,7 @@ import { API_CONFIG } from './config' import { createApiError, NetworkError, TimeoutError } from './errors' import type { ApiClient, ApiRequestConfig, HttpMethod } from './types' +import { authEvents, AUTH_EVENTS } from '../events' export class BaseApiClient implements ApiClient { private refreshPromise: Promise | null = null @@ -147,6 +148,9 @@ export class BaseApiClient implements ApiClient { if (!response.ok) { 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 { diff --git a/src/lib/events.ts b/src/lib/events.ts new file mode 100644 index 0000000..0ee1086 --- /dev/null +++ b/src/lib/events.ts @@ -0,0 +1,43 @@ +/** + * Simple event emitter for cross-component communication + */ + +type EventHandler = (...args: any[]) => void + +class EventEmitter { + private events: Map = 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 \ No newline at end of file diff --git a/src/lib/token-refresh-manager.ts b/src/lib/token-refresh-manager.ts new file mode 100644 index 0000000..ba011b0 --- /dev/null +++ b/src/lib/token-refresh-manager.ts @@ -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 => { + 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 => { + 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() \ No newline at end of file diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index eff225d..0bb5763 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -1,5 +1,6 @@ import { useAuth } from '../contexts/AuthContext' import { ModeToggle } from '../components/ModeToggle' +import { SocketStatus } from '../components/SocketStatus' export function DashboardPage() { const { user, logout } = useAuth() @@ -28,8 +29,11 @@ export function DashboardPage() {
-
-

Dashboard content coming soon...

+
+ +
+

Dashboard content coming soon...

+