USER
У меня есть клиент-серверное приложение с взаимодействием с базой данных и есть небольшая структура из путей, контроллеров, моделей и 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, а когда тот устаревает, то выкидываем пользователя обратно в логин
ASSISTANT
Для защиты вашего эндпоинта получения проектов (`/api/cards`) с использованием JWT, необходимо добавить middleware на сервере, который будет проверять наличие и валидность JWT в заголовках запросов. Ниже приведены шаги для реализации этой функциональности:
1. **Создание Middleware для проверки JWT**
2. **Применение Middleware к защищённым маршрутам**
3. **Обработка ошибок и ответов**
### 1. Создание Middleware для проверки JWT
Создайте файл `authMiddleware.js` в папке `middleware` (если её ещё нет):
```javascript
// middleware/authMiddleware.js
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
dotenv.config();
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
export const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
// Проверяем, присутствует ли заголовок Authorization
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ message: "Токен не предоставлен" });
}
jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
if (err) {
console.log("JWT валидация не удалась:", err);
return res.status(403).json({ message: "Неверный токен" });
}
req.user = user; // Добавляем информацию о пользователе в запрос
next();
});
};
```
**Объяснение:**
- **Получение токена:** Middleware извлекает токен из заголовка `Authorization`. Он ожидает формат `Bearer <token>`.
- **Валидация токена:** Использует `jwt.verify` для проверки валидности токена. Если токен неверен или истёк, возвращается соответствующий статус.
- **Передача данных о пользователе:** Если токен валиден, информация о пользователе (`userId`, `username`) добавляется в объект `req` для использования в контроллере.
### 2. Применение Middleware к защищённым маршрутам
Теперь, когда у вас есть middleware для аутентификации, применим его к маршрутам, которые должны быть защищены. В вашем случае это `/api/cards`.
```javascript
// routes/projectRoutes.js
import express from "express";
import { getCards } from "../controller/projectController.js";
import { authenticateToken } from "../middleware/authMiddleware.js";
const router = express.Router();
// Применяем middleware authenticateToken к маршруту /cards
router.get("/cards", authenticateToken, getCards);
export default router;
```
**Объяснение:**
- Маршрут `/cards` теперь защищён и требует действительного JWT для доступа.
- Middleware `authenticateToken` выполняется перед вызовом контроллера `getCards`. Если аутентификация не пройдена, контроллер не будет вызван.
### 3. Обработка ошибок и ответов
Убедитесь, что ваш контроллер `getCards` учитывает информацию о пользователе, доступную через `req.user`.
```javascript
// controller/projectController.js
import dotenv from "dotenv";
import { getProjects } from "../model/projectModel.js";
dotenv.config();
export const getCards = async (req, res) => {
try {
// Вы можете использовать информацию о пользователе из req.user, если необходимо
const userId = req.user.userId;
const username = req.user.username;
console.log(`Пользователь ${username} (ID: ${userId}) запрашивает карточки.`);
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" });
}
};
```
**Объяснение:**
- **Использование данных пользователя:** Вы можете использовать данные из `req.user`, например, для журналирования или ограничения доступа к определённым проектам.
- **Обработка успешного ответа и ошибок:** Контроллер продолжает свою работу, возвращая данные или обрабатывая возможные ошибки.
### Дополнительные рекомендации
1. **Обновление списка токенов на сервере:**
В вашем `authController.js` вы храните список `refreshTokens` в памяти. Это работает только для одного инстанса сервера и не сохраняется между перезапусками сервера. Рассмотрите возможность сохранения `refreshTokens` в базе данных для устойчивости и масштабируемости.
2. **Хранение токенов:**
Будьте осторожны с хранением токенов на клиенте. Убедитесь, что они хранятся безопасно, используя безопасные куки (`HttpOnly`, `Secure`) вместо `localStorage` для повышения безопасности.
3. **Обработка ролей и разрешений:**
Если ваше приложение требует более сложной авторизации (например, роли пользователей), расширьте `authenticateToken` для проверки прав доступа.
4. **Установка более длительного времени жизни токенов:**
В вашем текущем настроении `accessToken` истекает через 30 секунд, что очень коротко. Обычно `accessToken` имеет время жизни от нескольких минут до часа. Подберите оптимальное время, балансируя между безопасностью и удобством пользователя.
5. **Логирование и мониторинг:**
Реализуйте систему логирования для отслеживания успешных и неуспешных аутентификаций, обновлений токенов и других важных действий.
### Полный пример с добавленным Middleware
Для наглядности предоставляю полный пример сочетания всех компонентов:
**1. Middleware для аутентификации:**
```javascript
// middleware/authMiddleware.js
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
dotenv.config();
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
export const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ message: "Токен не предоставлен" });
}
jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
if (err) {
console.log("JWT валидация не удалась:", err);
return res.status(403).json({ message: "Неверный токен" });
}
req.user = user;
next();
});
};
```
**2. Применение Middleware к защищённому маршруту:**
```javascript
// routes/projectRoutes.js
import express from "express";
import { getCards } from "../controller/projectController.js";
import { authenticateToken } from "../middleware/authMiddleware.js";
const router = express.Router();
// Защищённый маршрут
router.get("/cards", authenticateToken, getCards);
export default router;
```
**3. Контроллер с использованием данных пользователя:**
```javascript
// controller/projectController.js
import dotenv from "dotenv";
import { getProjects } from "../model/projectModel.js";
dotenv.config();
export const getCards = async (req, res) => {
try {
const userId = req.user.userId;
const username = req.user.username;
console.log(`Пользователь ${username} (ID: ${userId}) запрашивает карточки.`);
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" });
}
};
```
### Итог
Добавив middleware для проверки JWT на сервере и применив его к маршрутам, которые должны быть защищены, вы обеспечите безопасность доступа к этим маршрутам. Ваш текущий клиентский код уже способен обрабатывать обновление `accessToken` при получении ошибки 401, поэтому интеграция с серверной частью будет корректной после добавления серверного middleware.
Если у вас возникнут дополнительные вопросы или потребуется помощь с другими аспектами приложения, не стесняйтесь обращаться!