diff --git a/src/app/components/AuthInitializer.tsx b/src/app/components/AuthInitializer.tsx index 5f26164..f158f15 100644 --- a/src/app/components/AuthInitializer.tsx +++ b/src/app/components/AuthInitializer.tsx @@ -3,8 +3,20 @@ import { useEffect } from 'react' import useAuthStore from '../store/authStore' +// Helper to decode JWT and get expiry +function getTokenExpiry(token: string | null): Date | null { + if (!token) return null; + try { + const [, payload] = token.split("."); + const { exp } = JSON.parse(atob(payload)); + return exp ? new Date(exp * 1000) : null; + } catch { + return null; + } +} + export default function AuthInitializer({ children }: { children: React.ReactNode }) { - const { refreshAuthToken, setAuthReady } = useAuthStore() + const { refreshAuthToken, setAuthReady, accessToken } = useAuthStore() useEffect(() => { const initializeAuth = async () => { @@ -22,5 +34,39 @@ export default function AuthInitializer({ children }: { children: React.ReactNod initializeAuth() }, [refreshAuthToken, setAuthReady]) + // Automatic token refresh - check every minute + useEffect(() => { + const interval = setInterval(async () => { + const currentToken = useAuthStore.getState().accessToken; + if (currentToken) { + const expiry = getTokenExpiry(currentToken); + if (expiry) { + const timeUntilExpiry = expiry.getTime() - Date.now(); + const threeMinutes = 3 * 60 * 1000; // 3 minutes in milliseconds + + // If token expires within 3 minutes, refresh it + if (timeUntilExpiry <= threeMinutes && timeUntilExpiry > 0) { + console.log('๐Ÿ”„ Token expires soon, auto-refreshing...', { + expiresIn: Math.round(timeUntilExpiry / 1000), + expiresAt: expiry.toLocaleTimeString() + }); + try { + const success = await refreshAuthToken(); + if (success) { + console.log('โœ… Token auto-refresh successful'); + } else { + console.log('โŒ Token auto-refresh failed'); + } + } catch (error) { + console.log('โŒ Token auto-refresh error:', error); + } + } + } + } + }, 60000); // Check every minute + + return () => clearInterval(interval); + }, [refreshAuthToken]) + return <>{children} } \ No newline at end of file diff --git a/src/app/store/authStore.ts b/src/app/store/authStore.ts index 67f5ab2..da30408 100644 --- a/src/app/store/authStore.ts +++ b/src/app/store/authStore.ts @@ -203,6 +203,14 @@ const useAuthStore = create((set, get) => ({ log("โœ… Zustand: Refresh succeeded, setting in-memory token and user"); get().setAccessToken(body.accessToken); if (body.user) get().setUser(body.user); + + // Log token expiry for debugging + const newExpiry = getTokenExpiry(body.accessToken); + if (newExpiry) { + log("โฐ Zustand: New token expires at:", newExpiry.toLocaleString()); + log("โฐ Zustand: Time until expiry:", Math.round((newExpiry.getTime() - Date.now()) / 60000), "minutes"); + } + return true; } else { log("โŒ Zustand: Refresh failed (no accessToken or non-ok). Clearing auth state"); diff --git a/src/app/test-refresh/page.tsx b/src/app/test-refresh/page.tsx new file mode 100644 index 0000000..ea3a254 --- /dev/null +++ b/src/app/test-refresh/page.tsx @@ -0,0 +1,128 @@ +'use client' + +import { useState, useEffect } from 'react' +import useAuthStore from '../store/authStore' + +// Helper to decode JWT and get expiry +function getTokenExpiry(token: string | null): Date | null { + if (!token) return null; + try { + const [, payload] = token.split("."); + const { exp } = JSON.parse(atob(payload)); + return exp ? new Date(exp * 1000) : null; + } catch { + return null; + } +} + +export default function TestRefreshPage() { + const { accessToken, refreshAuthToken, user } = useAuthStore() + const [tokenInfo, setTokenInfo] = useState({}) + const [refreshStatus, setRefreshStatus] = useState('') + + // Update token info every second + useEffect(() => { + const interval = setInterval(() => { + if (accessToken) { + const expiry = getTokenExpiry(accessToken) + const now = Date.now() + const timeLeft = expiry ? expiry.getTime() - now : 0 + + setTokenInfo({ + hasToken: !!accessToken, + tokenPrefix: accessToken ? `${accessToken.substring(0, 20)}...` : null, + expiresAt: expiry ? expiry.toLocaleTimeString() : 'Unknown', + timeLeftMs: timeLeft, + timeLeftMin: Math.round(timeLeft / 60000), + timeLeftSec: Math.round(timeLeft / 1000), + isExpired: timeLeft <= 0 + }) + } else { + setTokenInfo({ hasToken: false }) + } + }, 1000) + + return () => clearInterval(interval) + }, [accessToken]) + + const handleManualRefresh = async () => { + setRefreshStatus('Refreshing...') + try { + const success = await refreshAuthToken() + setRefreshStatus(success ? 'โœ… Success' : 'โŒ Failed') + } catch (error) { + setRefreshStatus(`โŒ Error: ${error}`) + } + setTimeout(() => setRefreshStatus(''), 3000) + } + + return ( +
+
+

๐Ÿงช Token Refresh Test

+ +
+ {/* Token Status */} +
+

๐Ÿ”‘ Token Status

+
+
Has Token: + {tokenInfo.hasToken ? 'โœ… Yes' : 'โŒ No'} +
+ + {tokenInfo.hasToken && ( + <> +
Token Preview: {tokenInfo.tokenPrefix}
+
Expires At: {tokenInfo.expiresAt}
+
Time Left: + {tokenInfo.timeLeftMin}m {tokenInfo.timeLeftSec % 60}s +
+
Status: + {tokenInfo.isExpired ? '๐Ÿ’€ Expired' : 'โœ… Valid'} +
+ + )} +
+
+ + {/* User Info */} +
+

๐Ÿ‘ค User Info

+
+              {JSON.stringify(user, null, 2)}
+            
+
+ + {/* Manual Controls */} +
+

๐Ÿ”ง Manual Controls

+
+ + {refreshStatus && ( +
{refreshStatus}
+ )} +
+
+ + {/* Instructions */} +
+

๐Ÿ“‹ Testing Instructions

+
    +
  1. Make sure JWT_EXPIRES_IN=2m in backend .env for fast testing
  2. +
  3. Login and watch the countdown timer
  4. +
  5. When time left โ‰ค 3 minutes, auto-refresh should trigger
  6. +
  7. Check browser console for detailed logs
  8. +
  9. Check Network tab for /api/refresh requests
  10. +
  11. Token should automatically renew without user action
  12. +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/app/utils/authFetch.ts b/src/app/utils/authFetch.ts index 1d87cb3..f24105b 100644 --- a/src/app/utils/authFetch.ts +++ b/src/app/utils/authFetch.ts @@ -46,17 +46,18 @@ interface CustomRequestInit extends RequestInit { // Main authFetch function export async function authFetch(input: RequestInfo | URL, init: CustomRequestInit = {}): Promise { - const accessToken = getAccessToken(); + // Always get the fresh token from store at call time + let accessToken = getAccessToken(); const url = typeof input === "string" ? input : input instanceof URL ? input.href : (input as Request).url; log("๐ŸŒ authFetch: Making API call to:", url); log("๐Ÿ”‘ authFetch: Using token:", accessToken ? `${accessToken.substring(0, 20)}...` : "No token"); // Add Authorization header if accessToken exists - const headers: Record = { + const buildHeaders = (token: string | null): Record => ({ ...(init.headers || {}), - ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), - }; + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }); // Always send credentials so refresh cookie is included when server-side refresh is needed const fetchWithAuth = async (hdrs: Record): Promise => { @@ -66,7 +67,7 @@ export async function authFetch(input: RequestInfo | URL, init: CustomRequestIni let res: Response; try { - res = await fetchWithAuth(headers); + res = await fetchWithAuth(buildHeaders(accessToken)); } catch (err) { log("โŒ authFetch: Network/error calling API:", err); throw err; @@ -82,17 +83,12 @@ export async function authFetch(input: RequestInfo | URL, init: CustomRequestIni log("๐Ÿ”„ authFetch: store.refreshAuthToken() result:", refreshOk); if (refreshOk) { - // get new token from memory (store already set it) + // get FRESH token from memory (store already set it) const newToken = getAccessToken(); log("๐Ÿ” authFetch: Retrieved new token from store after refresh:", newToken ? `${newToken.substring(0,20)}...` : null); - const retryHeaders = { - ...headers, - ...(newToken ? { Authorization: `Bearer ${newToken}` } : {}), - }; - log("๐Ÿ”„ authFetch: Retrying original request with refreshed token (if available)"); - res = await fetch(url, { ...init, headers: retryHeaders, credentials: "include" }); + res = await fetch(url, { ...init, headers: buildHeaders(newToken), credentials: "include" }); log("๐Ÿ“ก authFetch: Retry response status:", res.status); } else { log("โŒ authFetch: Refresh failed. Calling logout to revoke server cookie and clear client state");