// src/app/(main)/axios/AxiosProvider.tsx "use client"; import React, { createContext, useContext, useEffect, useState, useCallback } from "react"; import { getSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { isBackendJwtExpired, LOGIN_SESSION_EXPIRED_HREF, } from "@/app/utils/authToken"; import axiosInstance, { SetupAxiosInterceptors } from "./axiosInstance"; const AxiosContext = createContext(axiosInstance); const TokenContext = createContext<{ setAccessToken: (token: string | null) => void; }>({ setAccessToken: () => {}, }); export const AxiosProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [accessToken, setAccessToken] = useState(null); const [isHydrated, setIsHydrated] = useState(false); // Hydrate token only on client useEffect(() => { try { const token = localStorage.getItem("accessToken"); if (token) setAccessToken(token); } catch (e) { console.warn("localStorage unavailable", e); } finally { setIsHydrated(true); } }, []); /** * Detect expired/missing backend JWT before user actions (e.g. /report search). * Sync accessToken from next-auth session into localStorage if missing, then * redirect to login when the Bearer token is absent or past `exp`. */ useEffect(() => { if (!isHydrated || typeof window === "undefined") return; let cancelled = false; (async () => { try { let token = localStorage.getItem("accessToken")?.trim() ?? ""; if (!token) { const session = (await getSession()) as SessionWithTokens | null; if (cancelled) return; if (session?.accessToken) { token = session.accessToken; localStorage.setItem("accessToken", token); setAccessToken(token); } } if (!token) { window.location.href = LOGIN_SESSION_EXPIRED_HREF; return; } if (isBackendJwtExpired(token)) { window.location.href = LOGIN_SESSION_EXPIRED_HREF; } } catch (e) { console.warn("Auth token check failed", e); } })(); return () => { cancelled = true; }; }, [isHydrated]); // Apply token + interceptors useEffect(() => { if (accessToken) { axiosInstance.defaults.headers.Authorization = `Bearer ${accessToken}`; SetupAxiosInterceptors(accessToken); } else { delete axiosInstance.defaults.headers.Authorization; } }, [accessToken]); const handleSetAccessToken = useCallback((token: string | null) => { setAccessToken(token); try { if (token) { localStorage.setItem("accessToken", token); } else { localStorage.removeItem("accessToken"); } } catch (e) { // ignore (e.g. private mode) } }, []); // Critical fix: never return null → always return children wrapped in fragment return ( {/* Render children immediately – they will just not have the token for 1-2ms */} {children} ); }; export const useAxios = () => useContext(AxiosContext); export const useToken = () => useContext(TokenContext);