4. Implement authentication logic using JWT strategy (POST
/login endpoint).
a. Use the jsonwebtoken library for JWT operations.
b. JWT should be implemented with access and refresh
tokens.
c. Tokens should not be stored in the database.
5. Unauthorized users should be redirected to login if they are
unauthorized or if tokens are expired.
a. If the access token is expired (server sends a 401 status
error) but the refresh token is valid, then the frontend
should automatically refresh the access token and
repeat failed requests.
6. The endpoint for getting projects should be protected by
JWT.


У меня есть вот такой набор заданий к моему клиент-серверному приложению, которое использует React/Redux/ExpressJS.

Я начал вводить логику работы с JWT токенами в код, но сбился с пути и забыл про ход своих мыслей, я буду отправлять тебе код, а ты помоги мне реализовать все эти задания. ДАВАЙ ПОКА ЧТО ЗАЙМЁМСЯ ТОЛЬКО ЛОГИКОЙ, СВЯЗАННОЙ С ЛОГИНОМ, А КАРТОЧКИ ТРОГАТЬ НЕ БУДЕМ

Сервер:
import express from "express";
import dotenv from "dotenv";
import cors from "cors";
import pkg from "pg";
const { Client } = pkg;
import bcrypt from "bcrypt";
import knex from "knex";
import jwt from "jsonwebtoken";

dotenv.config();

const postgres = knex({
  client: "pg",
  connection: process.env.LOCALDB_URL,
});

const port = process.env.PORT || 5000;
const app = express();

app.use(cors());
app.use("/img", express.static("./img"));
app.use(express.json());

app.get("/api/test", (_, res) => {
  res.json({ message: "Server connected to frontend" });
});

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

let refreshTokens = [];

function authenticateToken(req, res, next) {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];

  if (!token) return res.status(401).json({ message: "Token not provided" });

  jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) return res.status(401).json({ message: "Invalid tokn" });
    req.user = user;
    next();
  });
}

app.post("/api/login", async (req, res) => {
  const { username, password } = req.body;

  try {
    const user = await postgres("users").where({ username }).first();

    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: "15m",
    });
    const refreshToken = jwt.sign(userPayload, REFRESH_TOKEN_SECRET, {
      expiresIn: "7d",
    });

    refreshTokens.push(refreshToken);

    res.status(200).json({
      accessToken,
      refreshToken,
      user: {
        userId: user.id,
        username: user.username,
      },
    });
  } catch (error) {
    console.error("Ошибка при логине:", error);
    res.status(500).json({ message: "Ошибка сервера" });
  }
});

app.post("/api/token", (req, res) => {
  const { refreshToken } = req.body;

  if (!refreshToken)
    return res.status(401).json({ message: "Token not provided" });
  if (!refreshTokens.includes(refreshToken))
    return res.status(403).json({ message: "Invalid token" });

  jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) return res.status(403).json({ message: "Invalid Token" });

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

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

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

app.post("/api/logout", (req, res) => {
  const { refreshToken } = req.body;
  refreshTokens = refreshTokens.filter((token) => token !== refreshToken);
  res.status(200).json({ message: "User quit app" });
});

app.get("/api/cards", authenticateToken, async (req, res) => {
  try {
    const searchTerm = req.query.search ? req.query.search.toLowerCase() : "";

    let projectsQuery = postgres("projects").select("*");
    if (searchTerm) {
      projectsQuery = projectsQuery.where(function () {
        this.whereRaw("LOWER(title) LIKE ?", [`%${searchTerm}%`]).orWhereRaw(
          "LOWER(description) LIKE ?",
          [`%${searchTerm}%`]
        );
      });
    }
    const projects = await projectsQuery;

    const apiBaseUrl =
      process.env.LOCALSERVER ?? `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 fetching projects:", error);
    res.status(500).json({ error: "Failed to fetch projects" });
  }
});

app.post("/api/signup", async (req, res) => {
  const { firstName, lastName, username, password, confirmPassword, age } =
    req.body;

  const errors = {};

  if (!firstName || firstName.trim().length < 3) {
    errors.firstName = "First name must be at least 3 characters long.";
  }

  if (!lastName || lastName.trim().length < 3) {
    errors.lastName = "Last name must be at least 3 characters long.";
  }

  if (!username || username.trim().length < 3) {
    errors.username = "Username must be at least 3 characters long.";
  }

  if (!password) {
    errors.password = "Please enter a password.";
  } else {
    if (password.length < 4) {
      errors.password = "Password must be at least 4 characters long.";
    }
    if (!/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) {
      errors.password =
        "Password must contain at least one letter and one number.";
    }
  }

  if (!confirmPassword) {
    errors.confirmPassword = "Please confirm your password.";
  } else if (password !== confirmPassword) {
    errors.confirmPassword = "Passwords do not match.";
  }

  const ageNumber = Number(age);
  if (!age) {
    errors.age = "Please specify your age.";
  } else if (isNaN(ageNumber)) {
    errors.age = "Age must be a number.";
  } else if (ageNumber <= 0) {
    errors.age = "Age cannot be zero or negative.";
  }

  if (Object.keys(errors).length > 0) {
    return res.status(400).json({ errors });
  }

  try {
    const existingUser = await postgres("users").where({ username }).first();

    if (existingUser) {
      return res
        .status(400)
        .json({ errors: { username: "Username is already taken." } });
    }

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

    await postgres("users").insert({
      firstName,
      lastName,
      username,
      password: hashedPassword,
      age: ageNumber,
    });

    res.status(201).json({ message: "User registered successfully" });
  } catch (error) {
    console.error("Error during registration:", error);
    res.status(500).json({ message: "Server error" });
  }
});

app.listen(port, () => console.log(`Server is running on port ${port}`));

const client = new Client({
  connectionString: process.env.LOCALDB_URL,
});

async function connectToDatabase() {
  try {
    await client.connect();
    console.log("Successfully connected to the database");
  } catch (error) {
    console.error("Error connecting to the database:", error);
  } finally {
    await client.end();
  }
}

connectToDatabase();



Файл со всеми экшенами: 
import {
  login,
  logout,
  searchTerm as search,
  loginError as logErr,
} from "./types";

export const loginThunk = (username, password) => async (dispatch) => {
  try {
    const response = await fetch("http://localhost:5000/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.setItem("accessToken", accessToken);
      localStorage.setItem("refreshToken", refreshToken);
      localStorage.setItem("user", JSON.stringify(user));

      dispatch(loginAction(data));
      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,
    value: searchTerm,
  };
};

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

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

export const loginAction = (user) => {
  return {
    type: login,
    value: user,
  };
};

export const fetchSearchItems = (searchTerm) => {
  return async (dispatch) => {
    try {
      const response = await fetch(
        `http://localhost:5000/api/cards?search=${searchTerm}`
      );
      const data = await response.json();
      dispatch({
        type: "set_filtered_objects",
        value: data,
      });
    } catch (error) {
      console.log("Happened error: \n", error);
    }
  };
};

export const fetchAllItems = () => {
  return async (dispatch) => {
    try {
      const response = await fetch(`http://localhost:5000/api/cards`);
      const data = await response.json();
      dispatch({
        type: "set_filtered_objects",
        value: data,
      });
    } catch (error) {
      console.log("Happened error: \n" + error);
    }
  };
};


Файл редюсеров:
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.value };
    case setFiltered:
      return { ...state, filteredObjects: action.value };
    default:
      return state;
  }
};

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

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



Login.jsx: 
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>
  );
}



Файл, который я назвал jwtFunctions:
import { logoutAction } from "../store/actions/actions";

async function fetchWithAuth(url, options = {}) {
  let accessToken = localStorage.getItem("accessToken");

  if (!options.headers) {
    options.headers = {};
  }
  options.headers["Authorization"] = `Bearer ${accessToken}`;

  let response = await fetch(url, options);

  if (response.status === 401) {
    const refreshToken = localStorage.getItem("refreshToken");

    const tokenResponse = await fetch("http://localhost:5000/api/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ refreshToken }),
    });

    if (tokenResponse.ok) {
      const tokenData = await tokenResponse.json();
      accessToken = tokenData.accessToken;
      localStorage.setItem("accessToken", accessToken);

      options.headers["Authorization"] = `Bearer ${accessToken}`;
      response = await fetch(url, options);
    } else {
      localStorage.removeItem("accessToken");
      localStorage.removeItem("refreshToken");
      localStorage.removeItem("user");
      window.location.href = "/login";
      return;
    }
  }

  return response;
}

export const getCards = () => async (dispatch) => {
  try {
    const response = await fetchWithAuth("http://localhost:5000/api/cards");

    if (response.ok) {
      const data = await response.json();
      dispatch(setCards(data));
    } else {
      const errorData = await response.json();
      console.error("Ошибка при получении карточек:", errorData);
    }
  } catch (error) {
    console.error("Ошибка при получении карточек:", error);
  }
};

export const logoutUser = () => async (dispatch) => {
  const refreshToken = localStorage.getItem("refreshToken");

  await fetch("http://localhost:5000/api/logout", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refreshToken }),
  });

  localStorage.removeItem("accessToken");
  localStorage.removeItem("refreshToken");
  dispatch(logoutAction());
  //redirect
  window.location.href = "/login";
};