У меня есть клиент-серверное приложение с взаимодействием с базой данных и есть небольшая структура из путей, контроллеров, моделей и middleware. Мне нужно, чтобы ты помог мне с одним заданием, сейчас я предоставлю тебе весь код, который нам понадобится:
Файл jwtFunctions из папки клиента:
export const fetchWithAuth = async (url, options = {}) => {
  let accessToken = localStorage.getItem("accessToken");
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      authorization: `Bearer ${accessToken}`,
    },
  });

  if (response.status === 401) {
    accessToken = await refreshAccessToken();

    if (accessToken) {
      response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${accessToken}`,
        },
      });
    } else {
      throw new Error("Unable to refresh access token");
    }
  }

  return response;
};

export const refreshAccessToken = async () => {
  console.log("Refresh Access Token start");
  const refreshToken = localStorage.getItem("refreshToken");
  console.log("Retrieved Refresh Token:", refreshToken);
  if (!refreshToken) {
    console.log("Refresh token not found");
    return null;
  }

  try {
    const response = await fetch(
      `https://server-ancient-grass-9030.fly.dev/api/token`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ refreshToken }),
      }
    );

    if (response.ok) {
      const data = await response.json();
      const newAccessToken = data.accessToken;
      localStorage.setItem("accessToken", newAccessToken);
      return newAccessToken;
    } else {
      console.log("Failed to refresh access token");
      return null;
    }
  } catch (error) {
    console.error("An error occurred while refreshing access token:", error);
    return null;
  }
};


Компонент Login:
import logo from "../../../public/assets/images/spring.png";
import LoginInput from "./loginInput/loginInput";
import LoginButton from "./loginButton/LoginButton";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { loginThunk } from "../../store/actions/actions";

export default function Login() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState({ error: "" });

  const dispatch = useDispatch();
  const navigate = useNavigate();

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError({ error: "" });

    const result = await dispatch(loginThunk(username, password));

    if (result.success) {
      navigate("/");
    } else {
      setError({ error: result.message });
    }
  };

  const handleSignupRedirect = (e) => {
    e.preventDefault();
    navigate("/signup");
  };

  return (
    <div className="login">
      <div className="login__header">
        <img className="login__header__image" src={logo} alt="logo_icon" />
      </div>

      <div className="login__card">
        <div className="login__card__header">
          <span className="login__card__header__sentence">
            Please, Enter your details
          </span>
          <h4 className="login__card__header__title">Welcome</h4>
        </div>
        <form className="login__card__info" onSubmit={handleSubmit}>
          <div className="login__card__info__inputs">
            <LoginInput
              type="text"
              value={username}
              placeholder="Enter your username"
              onChange={(e) => setUsername(e.target.value)}
            />
            <LoginInput
              type="password"
              value={password}
              placeholder="Enter your password"
              onChange={(e) => setPassword(e.target.value)}
            />
            <span className="login__error">{error.error}</span>
          </div>
          <div className="login__card__info__buttons">
            <LoginButton type="submit" label="Submit" />
            <LoginButton
              type="button"
              label="Create an account"
              onClick={handleSignupRedirect}
            />
          </div>
        </form>
      </div>
    </div>
  );
}


Redux экшены:
import {
  login,
  logout,
  searchTerm as search,
  loginError as logErr,
} from "./types";
import { refreshAccessToken } from "../../components/jwtFunctions";
import { fetchWithAuth } 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,
  };
};

//That hurts me so bad
export const fetchSearchItems = (searchTerm) => {
  return async (dispatch) => {
    try {
      const accessToken = localStorage.getItem("accessToken");

      const response = await fetch(
        `https://server-ancient-grass-9030.fly.dev/api/cards?search=${encodeURIComponent(searchTerm)}`,
        {
          method: "GET",
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      );

      if (response.status === 401) {
        const newAccessToken = await refreshAccessToken();
        if (newAccessToken) {
          const retryResponse = await fetch(
            `https://server-ancient-grass-9030.fly.dev/api/cards?search=${encodeURIComponent(searchTerm)}`,
            {
              method: "GET",
              headers: {
                Authorization: `Bearer ${newAccessToken}`,
              },
            }
          );
          const data = await retryResponse.json();
          dispatch({
            type: "set_filtered_objects",
            payload: data,
          });
        } else {
          dispatch(logoutAction());
        }
      } else {
        const data = await response.json();
        dispatch({
          type: "set_filtered_objects",
          payload: data,
        });
      }
    } catch (error) {
      console.log("An error occurred: \n", error);
    }
  };
};

export const fetchAllItems = () => {
  return async (dispatch) => {
    try {
      const accessToken = localStorage.getItem("accessToken");

      const response = await fetch(
        `https://server-ancient-grass-9030.fly.dev/api/cards`,
        {
          method: "GET",
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      );

      if (response.status === 401) {
        const newAccessToken = await refreshAccessToken();
        if (newAccessToken) {
          const retryResponse = await fetch(
            `https://server-ancient-grass-9030.fly.dev/api/cards`,
            {
              method: "GET",
              headers: {
                Authorization: `Bearer ${newAccessToken}`,
              },
            }
          );
          const data = await retryResponse.json();
          dispatch({
            type: "set_filtered_objects",
            payload: data,
          });
        } else {
          dispatch(logoutAction());
        }
      } else {
        const data = await response.json();
        dispatch({
          type: "set_filtered_objects",
          payload: data,
        });
      }
    } catch (error) {
      console.log("An error occurred: \n", error);
    }
  };
};

Дальше серверная часть:
authRoutes.js
import express from "express";
import {
  login,
  refreshToken,
  logout,
  signup,
} from "../controller/authController.js";
import { validateSignup } from "../middleware/validationMiddleware.js";

const router = express.Router();

router.post("/login", login);
router.post("/token", refreshToken);
router.post("/logout", logout);
router.post("/signup", validateSignup, signup);

export default router;


authController:

import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
import { findUserByUsername, createUser } from "../model/userModel.js";

dotenv.config();

const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;

let refreshTokens = [];

export const login = async (req, res) => {
  const { username, password } = req.body;

  try {
    const user = await findUserByUsername(username);

    if (!user) {
      return res.status(401).json({ message: "Неверные учетные данные" });
    }

    const isPasswordValid = await bcrypt.compare(password, user.password);

    if (!isPasswordValid) {
      return res.status(401).json({ message: "Неверные учетные данные" });
    }

    const userPayload = {
      userId: user.id,
      username: user.username,
    };

    const accessToken = jwt.sign(userPayload, ACCESS_TOKEN_SECRET, {
      expiresIn: "30s",
    });

    const refreshToken = jwt.sign(userPayload, REFRESH_TOKEN_SECRET, {
      expiresIn: "2m",
    });
    console.log("Generated Refresh Token:", refreshToken);
    refreshTokens.push(refreshToken);
    console.log("Current Refresh Tokens:", refreshTokens);

    res.status(200).json({
      accessToken,
      refreshToken,
      user: {
        userId: user.id,
        username: user.username,
      },
    });
  } catch (error) {
    console.error("Ошибка входа:", error);
  }
};

export const refreshToken = (req, res) => {
  const { refreshToken } = req.body;

  console.log("Received Refresh Token for Refresh:", refreshToken);
  console.log("Valid Refresh Tokens:", refreshTokens.includes(refreshToken));
  if (!refreshToken)
    return res.status(401).json({ message: "Токен не предоставлен" });
  if (!refreshTokens.includes(refreshToken))
    return res.status(403).json({ message: "Недействительный токен" });

  jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) return res.status(403).json({ message: "Недействительный токен" });

    const userPayload = {
      userId: user.userId,
      username: user.username,
    };

    const newAccessToken = jwt.sign(userPayload, ACCESS_TOKEN_SECRET, {
      expiresIn: "30s",
    });

    res.json({ accessToken: newAccessToken });
  });
};

export const logout = (req, res) => {
  const { refreshToken } = req.body;
  refreshTokens = refreshTokens.filter((token) => token !== refreshToken);
  res.status(200).json({ message: "Выход из системы выполнен" });
};

export const signup = async (req, res) => {
  const { firstName, lastName, username, password, age } = req.body;

  try {
    const existingUser = await findUserByUsername(username);

    if (existingUser) {
      return res
        .status(400)
        .json({ errors: { username: "Имя пользователя уже занято." } });
    }

    const hashedPassword = await bcrypt.hash(password, 12);

    await createUser({
      firstName,
      lastName,
      username,
      password: hashedPassword,
      age,
    });

    res.status(201).json({ message: "Регистрация прошла успешно" });
  } catch (error) {
    console.error("Ошибка во время регистрации:", error);
    res.status(500).json({ message: "Ошибка сервера" });
  }
};


projectRoutes.js:
import express from "express";
import { getCards } from "../controller/projectController.js";

const router = express.Router();

router.get("/cards", getCards);

export default router;


projectController.js:
import dotenv from "dotenv";
import { getProjects } from "../model/projectModel.js";

dotenv.config();

export const getCards = async (req, res) => {
  try {
    const searchTerm = req.query.search ? req.query.search.toLowerCase() : "";

    const projects = await getProjects(searchTerm);

    const apiBaseUrl =
      process.env.REACT_APP_API_BASE_URL ??
      "https://server-ancient-grass-9030.fly.dev";
    const projectsWithImgSrc = projects.map((project) => ({
      ...project,
      imgSrc: `${apiBaseUrl}${project.img_src}`,
    }));

    res.json(projectsWithImgSrc);
  } catch (error) {
    console.error("Error getting projects:", error);
    res.status(500).json({ error: "Cannot get projects" });
  }
};


Вот задание: The endpoint for getting projects should be protected by
JWT.
То есть как я его понимаю, по истечении access токена мы регистрируем новый с помощью refresh, а когда тот устаревает, то выкидываем пользователя обратно в логин