|
- // 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<string | null>(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 (
- <AxiosContext.Provider value={axiosInstance}>
- <TokenContext.Provider value={{ setAccessToken: handleSetAccessToken }}>
- {/* Render children immediately – they will just not have the token for 1-2ms */}
- {children}
- </TokenContext.Provider>
- </AxiosContext.Provider>
- );
- };
-
- export const useAxios = () => useContext(AxiosContext);
- export const useToken = () => useContext(TokenContext);
|