Помоги мне сделать так, чтобы при обновлении страницы пользователя не выкидывало обратно в логин, пока его access token активен
Вот код:
app.jsx

import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import Main from "./components/Main/Main";
import Navbar from "./components/Navbar/Navbar";
import Login from "./components/Login/Login";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { logoutAction } from "./store/actions/actions";
import { loginThunk, loginAction } from "./store/actions/actions";
import Signup from "./components/Login/Signup";
import { fetchAllItems } from "./store/actions/actions";

const PrivateRoute = ({ children }) => {
  const { isAuth } = useSelector((state) => state.user);
  return isAuth ? children : <Navigate to="/login" />;
};

function App() {
  const dispatch = useDispatch();

  useEffect(() => {
    const user = JSON.parse(localStorage.getItem("user"));
    if (user) {
      dispatch(loginAction(user));
    }
  }, [dispatch]);

  useEffect(() => {
    dispatch(fetchAllItems());
  }, [dispatch]);

  useEffect(() => {
    fetch("https://server-ancient-grass-9030.fly.dev/api/test")
      .then((value) => value.json())
      .then((data) => console.log(data.message))
      .catch((message) => {
        throw new Error(message);
      });
  }, []);

  useEffect(() => {
    const storedUser = localStorage.getItem("user");
    if (storedUser) {
      dispatch(loginThunk(storedUser));
    } else {
      dispatch(logoutAction());
    }
  }, [dispatch]);

  return (
    <BrowserRouter basename="/ClientSpring/">
      <Routes>
        <Route
          path="/"
          element={
            <PrivateRoute>
              <>
                <Navbar />
                <Main />
              </>
            </PrivateRoute>
          }
        />
        <Route path="/signup" element={<Signup />} />
        <Route path="/login" element={<Login />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;


store.js

import { applyMiddleware, combineReducers, createStore } from "redux";
import { reducer, userReducer } from "./reducer";
import { thunk } from "redux-thunk";

const store = createStore(
  combineReducers({
    search: reducer,
    user: userReducer,
  }),
  applyMiddleware(thunk)
);

export default store;


reducer.js

import {
  login,
  logout,
  loginError,
  searchTerm,
  setFiltered,
} from "./actions/types";

const defaultState = {
  searchTerm: "",
  filteredObjects: [],
};

export const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case searchTerm:
      return { ...state, searchTerm: action.payload };
    case setFiltered:
      return { ...state, filteredObjects: action.payload };
    default:
      return state;
  }
};

const initialState = {
  isAuth: false,
  user: null,
  error: null,
};

export const userReducer = (state = initialState, action) => {
  switch (action.type) {
    case login:
      return {
        ...state,
        isAuth: true,
        user: action.payload,
        error: null,
      };
    case logout:
      return {
        ...state,
        isAuth: false,
        user: null,
        error: null,
      };
    case loginError:
      return {
        ...state,
        isAuth: false,
        user: null,
        error: action.payload,
      };
    default:
      return state;
  }
};


actions.js

import {
  login,
  logout,
  searchTerm as search,
  loginError as logErr,
} from "./types";
import { refreshAccessToken } from "../../components/jwtFunctions";

export const loginThunk = (username, password) => async (dispatch) => {
  try {
    const response = await fetch(
      "https://server-ancient-grass-9030.fly.dev/api/login",
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ username, password }),
      }
    );

    const data = await response.json();
    if (response.ok) {
      const { accessToken, refreshToken, user } = data;
      localStorage.removeItem("accessToken");
      localStorage.removeItem("refreshToken");
      localStorage.removeItem("user");

      localStorage.setItem("accessToken", accessToken);
      localStorage.setItem("refreshToken", refreshToken);
      localStorage.setItem("user", JSON.stringify(user));

      dispatch(loginAction(user));
      return { success: true };
    } else {
      dispatch(loginError(data.message));
      return { success: false, message: data.message };
    }
  } catch (error) {
    dispatch(loginError(error.toString()));
    return { success: false, message: error.toString() };
  }
};

export const setSearchTerm = (searchTerm) => {
  return {
    type: search,
    payload: searchTerm,
  };
};

export const logoutAction = () => {
  return {
    type: logout,
  };
};

export const loginError = (message) => {
  return {
    type: logErr,
    payload: message,
  };
};

export const loginAction = (user) => {
  return {
    type: login,
    payload: user,
  };
};
// Looks good. I'm kinda proud of myself(a little)
export const fetchSearchItems = (searchTerm) => {
  return (dispatch) => {
    dispatch(fetchItems(searchTerm));
  };
};

export const fetchAllItems = () => {
  return (dispatch) => {
    dispatch(fetchItems());
  };
};
export const fetchItems = (searchTerm = "") => {
  return async (dispatch) => {
    try {
      const accessToken = localStorage.getItem("accessToken");
      let url = `https://server-ancient-grass-9030.fly.dev/api/cards`;

      if (searchTerm) {
        url += `?search=${encodeURIComponent(searchTerm)}`;
      }

      let response = await fetch(url, {
        method: "GET",
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });
      if (response.status === 401 || response.status === 403) {
        const newAccessToken = await refreshAccessToken();
        if (newAccessToken) {
          response = await fetch(url, {
            method: "GET",
            headers: {
              Authorization: `Bearer ${newAccessToken}`,
            },
          });
        } else {
          dispatch(logoutAction());
          return;
        }
      }

      if (response.ok) {
        const data = await response.json();
        dispatch({
          type: "set_filtered_objects",
          payload: data,
        });
      } else {
        const errorData = await response.json();
        console.error("Error data fetching:", errorData.message);
      }
    } catch (error) {
      console.log("Happened error: \n", error);
    }
  };
};



Это клиент серверное приложение, где сервер создаёт refresh и access токены, если тебе нужен дополнительный код, дай знать