import { create } from "zustand"; import { log } from "../utils/logger"; // 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; } } // Helper functions for sessionStorage with SSR safety const getStoredUser = () => { if (typeof window === 'undefined') return null; // SSR check try { const userData = sessionStorage.getItem('user'); const parsed = userData ? JSON.parse(userData) : null; log("πŸ‘€ Retrieved user from sessionStorage:", parsed ? `${parsed.email || parsed.companyName}` : null); return parsed; } catch (error) { log("❌ Error retrieving user from sessionStorage:", error); return null; } }; const getStoredToken = () => { if (typeof window === 'undefined') return null; // SSR check try { const token = sessionStorage.getItem('accessToken'); if (token) { const expiry = getTokenExpiry(token); if (expiry && expiry.getTime() > Date.now()) { log("πŸ”‘ Retrieved valid token from sessionStorage"); return token; } else { log("⏰ Stored token expired, removing"); sessionStorage.removeItem('accessToken'); return null; } } return null; } catch (error) { log("❌ Error retrieving token from sessionStorage:", error); return null; } }; interface User { email?: string; companyName?: string; [key: string]: any; } interface AuthStore { accessToken: string | null; user: User | null; isAuthReady: boolean; isRefreshing: boolean; refreshPromise: Promise | null; setAuthReady: (ready: boolean) => void; setAccessToken: (token: string | null) => void; setUser: (userData: User | null) => void; clearAuth: () => void; logout: () => Promise; refreshAuthToken: () => Promise; getAuthState: () => AuthStore; } const useAuthStore = create((set, get) => ({ // Initialize with SSR-safe defaults accessToken: typeof window !== 'undefined' ? getStoredToken() : null, user: typeof window !== 'undefined' ? getStoredUser() : null, isAuthReady: false, isRefreshing: false, refreshPromise: null, setAuthReady: (ready) => { log("πŸ”” Zustand: setAuthReady ->", ready); set({ isAuthReady: !!ready }); }, setAccessToken: (token) => { log("πŸ”‘ Zustand: Setting access token in memory:", token ? `${token.substring(0, 20)}...` : null); if (typeof window !== 'undefined') { try { if (token) { const expiry = getTokenExpiry(token); log("⏳ Zustand: Token expiry:", expiry ? expiry.toLocaleString() : "Unknown"); sessionStorage.setItem('accessToken', token); log("βœ… Token stored in sessionStorage"); } else { sessionStorage.removeItem('accessToken'); log("πŸ—‘οΈ Token removed from sessionStorage"); } } catch (error) { log("❌ Error storing token in sessionStorage:", error); } } set({ accessToken: token }); }, setUser: (userData) => { log("πŸ‘€ Zustand: Setting user data:", userData ? `${userData.email || userData.companyName}` : null); if (typeof window !== 'undefined') { try { if (userData) { sessionStorage.setItem('user', JSON.stringify(userData)); log("βœ… User data stored in sessionStorage successfully"); } else { sessionStorage.removeItem('user'); log("πŸ—‘οΈ User data removed from sessionStorage"); } } catch (error) { log("❌ Error storing user in sessionStorage:", error); } } set({ user: userData }); }, clearAuth: () => { log("🧹 Zustand: Clearing all auth data from memory and removing persisted user"); if (typeof window !== 'undefined') { try { sessionStorage.removeItem('user'); log("βœ… User cleared from sessionStorage"); sessionStorage.removeItem('accessToken'); log("βœ… accessToken cleared from sessionStorage"); } catch (error) { log("❌ Error clearing user/accessToken from sessionStorage:", error); } } set({ accessToken: null, user: null }); }, logout: async () => { log("πŸšͺ Zustand: Logging out β€” revoking refresh token on server"); try { const logoutUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/logout`; log("🌐 Zustand: Calling logout endpoint:", logoutUrl); const res = await fetch(logoutUrl, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" } }); log("πŸ“‘ Logout response status:", res.status); try { const body = await res.json().catch(() => null); log("πŸ“¦ Logout response body:", body); } catch {} // Attempt to clear refreshToken cookie client-side (will only work if not httpOnly) if (typeof window !== 'undefined') { document.cookie = "refreshToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; SameSite=Strict"; } } catch (error) { log("❌ Error calling logout endpoint:", error); } finally { get().clearAuth(); get().setAuthReady(true); } }, refreshAuthToken: async () => { // If there's already a refresh in flight, return that promise if (get().refreshPromise) { log("πŸ” Zustand: refreshAuthToken - returning existing refresh promise"); return get().refreshPromise; } // SHORT-CIRCUIT: if we already have a valid accessToken that's not about to expire, skip refresh const currentToken = get().accessToken; if (currentToken) { const expiry = getTokenExpiry(currentToken); if (expiry && expiry.getTime() - Date.now() > 60 * 1000) { // more than 60s left log("⏸️ Zustand: accessToken present and valid, skipping refresh"); return Promise.resolve(true); } } log("πŸ”„ Zustand: refreshAuthToken - starting new refresh"); // create promise so concurrent callers can await it const p = (async () => { set({ isRefreshing: true }); try { const refreshUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/refresh`; log("🌐 Zustand: Calling refresh endpoint:", refreshUrl); const res = await fetch(refreshUrl, { method: "POST", credentials: "include" }); log("πŸ“‘ Zustand: Refresh response status:", res.status); const body = await res.json().catch(() => null); log("πŸ“¦ Zustand: Refresh response body:", body); if (res.ok && body && body.accessToken) { log("βœ… Zustand: Refresh succeeded, setting in-memory token and user"); get().setAccessToken(body.accessToken); if (body.user) get().setUser(body.user); return true; } else { log("❌ Zustand: Refresh failed (no accessToken or non-ok). Clearing auth state"); get().clearAuth(); return false; } } catch (error) { log("❌ Zustand: Refresh error:", error); get().clearAuth(); return false; } finally { set({ isRefreshing: false, refreshPromise: null }); log("πŸ”” Zustand: refreshAuthToken - finished (flags cleared)"); } })(); set({ refreshPromise: p }); return p; }, getAuthState: () => { const state = get(); log("πŸ“Š Current auth state:", { hasToken: !!state.accessToken, tokenPrefix: state.accessToken ? `${state.accessToken.substring(0, 20)}...` : null, user: state.user ? `${state.user.email || state.user.companyName}` : null }); return state; } })); export default useAuthStore;