FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

114 lines
3.3 KiB

  1. // src/app/(main)/axios/AxiosProvider.tsx
  2. "use client";
  3. import React, { createContext, useContext, useEffect, useState, useCallback } from "react";
  4. import { getSession } from "next-auth/react";
  5. import { SessionWithTokens } from "@/config/authConfig";
  6. import {
  7. isBackendJwtExpired,
  8. LOGIN_SESSION_EXPIRED_HREF,
  9. } from "@/app/utils/authToken";
  10. import axiosInstance, { SetupAxiosInterceptors } from "./axiosInstance";
  11. const AxiosContext = createContext(axiosInstance);
  12. const TokenContext = createContext<{
  13. setAccessToken: (token: string | null) => void;
  14. }>({
  15. setAccessToken: () => {},
  16. });
  17. export const AxiosProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  18. const [accessToken, setAccessToken] = useState<string | null>(null);
  19. const [isHydrated, setIsHydrated] = useState(false);
  20. // Hydrate token only on client
  21. useEffect(() => {
  22. try {
  23. const token = localStorage.getItem("accessToken");
  24. if (token) setAccessToken(token);
  25. } catch (e) {
  26. console.warn("localStorage unavailable", e);
  27. } finally {
  28. setIsHydrated(true);
  29. }
  30. }, []);
  31. /**
  32. * Detect expired/missing backend JWT before user actions (e.g. /report search).
  33. * Sync accessToken from next-auth session into localStorage if missing, then
  34. * redirect to login when the Bearer token is absent or past `exp`.
  35. */
  36. useEffect(() => {
  37. if (!isHydrated || typeof window === "undefined") return;
  38. let cancelled = false;
  39. (async () => {
  40. try {
  41. let token = localStorage.getItem("accessToken")?.trim() ?? "";
  42. if (!token) {
  43. const session = (await getSession()) as SessionWithTokens | null;
  44. if (cancelled) return;
  45. if (session?.accessToken) {
  46. token = session.accessToken;
  47. localStorage.setItem("accessToken", token);
  48. setAccessToken(token);
  49. }
  50. }
  51. if (!token) {
  52. window.location.href = LOGIN_SESSION_EXPIRED_HREF;
  53. return;
  54. }
  55. if (isBackendJwtExpired(token)) {
  56. window.location.href = LOGIN_SESSION_EXPIRED_HREF;
  57. }
  58. } catch (e) {
  59. console.warn("Auth token check failed", e);
  60. }
  61. })();
  62. return () => {
  63. cancelled = true;
  64. };
  65. }, [isHydrated]);
  66. // Apply token + interceptors
  67. useEffect(() => {
  68. if (accessToken) {
  69. axiosInstance.defaults.headers.Authorization = `Bearer ${accessToken}`;
  70. SetupAxiosInterceptors(accessToken);
  71. } else {
  72. delete axiosInstance.defaults.headers.Authorization;
  73. }
  74. }, [accessToken]);
  75. const handleSetAccessToken = useCallback((token: string | null) => {
  76. setAccessToken(token);
  77. try {
  78. if (token) {
  79. localStorage.setItem("accessToken", token);
  80. } else {
  81. localStorage.removeItem("accessToken");
  82. }
  83. } catch (e) {
  84. // ignore (e.g. private mode)
  85. }
  86. }, []);
  87. // Critical fix: never return null → always return children wrapped in fragment
  88. return (
  89. <AxiosContext.Provider value={axiosInstance}>
  90. <TokenContext.Provider value={{ setAccessToken: handleSetAccessToken }}>
  91. {/* Render children immediately – they will just not have the token for 1-2ms */}
  92. {children}
  93. </TokenContext.Provider>
  94. </AxiosContext.Provider>
  95. );
  96. };
  97. export const useAxios = () => useContext(AxiosContext);
  98. export const useToken = () => useContext(TokenContext);