Merge pull request #36 from SlimusMinus/fix-security
added refresh token
This commit is contained in:
85
src/api/apiClient.jsx
Normal file
85
src/api/apiClient.jsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { API_URL } from "../config/authConstants";
|
||||
|
||||
function getAccessToken() {
|
||||
return localStorage.getItem("token");
|
||||
}
|
||||
|
||||
function getRefreshToken() {
|
||||
return localStorage.getItem("refreshToken");
|
||||
}
|
||||
|
||||
function saveTokens(token, refreshToken) {
|
||||
localStorage.setItem("token", token);
|
||||
localStorage.setItem("refreshToken", refreshToken);
|
||||
}
|
||||
|
||||
function clearTokens() {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("refreshToken");
|
||||
localStorage.removeItem("user");
|
||||
}
|
||||
|
||||
let refreshPromise = null;
|
||||
|
||||
export async function refreshAccessToken() {
|
||||
// Если несколько запросов одновременно словили 401 — не долбим /refresh параллельно,
|
||||
// а ждём один и тот же промис
|
||||
if (refreshPromise) return refreshPromise;
|
||||
|
||||
refreshPromise = (async () => {
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) throw new Error("No refresh token");
|
||||
|
||||
const res = await fetch(`${API_URL}/api/v1/auth/refresh`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Refresh failed");
|
||||
|
||||
const data = await res.json();
|
||||
saveTokens(data.token, data.refreshToken);
|
||||
return data.token;
|
||||
})();
|
||||
|
||||
try {
|
||||
return await refreshPromise;
|
||||
} finally {
|
||||
refreshPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обёртка над fetch с авто-подстановкой Authorization и авто-обновлением
|
||||
* access-токена при 401. Использовать вместо голого fetch для защищённых эндпоинтов.
|
||||
*/
|
||||
export async function apiFetch(url, options = {}) {
|
||||
const token = getAccessToken();
|
||||
|
||||
const doFetch = (accessToken) =>
|
||||
fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
let res = await doFetch(token);
|
||||
|
||||
if (res.status === 401 && getRefreshToken()) {
|
||||
try {
|
||||
const newToken = await refreshAccessToken();
|
||||
res = await doFetch(newToken);
|
||||
} catch {
|
||||
clearTokens();
|
||||
window.location.href = "/";
|
||||
throw new Error("Session expired");
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export { getAccessToken, getRefreshToken, saveTokens, clearTokens };
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import CloseIcon from "./icons/CloseIcon";
|
||||
import { useAuthContext } from "../context/AuthContext";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { saveTokens } from "../api/apiClient";
|
||||
|
||||
/**
|
||||
* AuthModal — модальное окно входа и регистрации прямо на странице ленты.
|
||||
@@ -67,7 +68,8 @@ function LoginFormInline({ saveAuth, onClose, onSwitch }) {
|
||||
throw new Error(data.message || t('auth.invalidCredentials'));
|
||||
}
|
||||
|
||||
const { token, user } = await res.json();
|
||||
const { token, refreshToken, user } = await res.json();
|
||||
saveTokens(token, refreshToken);
|
||||
saveAuth(user, token);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
@@ -185,7 +187,8 @@ function RegisterFormInline({ saveAuth, onClose, onSwitch }) {
|
||||
|
||||
if (!loginRes.ok) throw new Error("Аккаунт создан. Войдите вручную.");
|
||||
|
||||
const { token, user } = await loginRes.json();
|
||||
const { token, refreshToken, user } = await loginRes.json();
|
||||
saveTokens(token, refreshToken);
|
||||
saveAuth(user, token);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { useAuthContext } from "../context/AuthContext";
|
||||
import { saveTokens } from "../api/apiClient";
|
||||
|
||||
export function useAuth() {
|
||||
const navigate = useNavigate();
|
||||
@@ -14,7 +15,7 @@ export function useAuth() {
|
||||
setLoading(true); setError("");
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/v1/auth`, {
|
||||
const res = await fetch(`${API_URL}/api/v1/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ login, password }),
|
||||
@@ -25,9 +26,16 @@ export function useAuth() {
|
||||
throw new Error(data.message || "Invalid login or password");
|
||||
}
|
||||
|
||||
const { token, user } = await res.json();
|
||||
const data = await res.json();
|
||||
|
||||
// Сохраняем в глобальный контекст + localStorage
|
||||
console.log("LOGIN RESPONSE", data);
|
||||
|
||||
const { token, refreshToken, user } = data;
|
||||
|
||||
console.log("ACCESS", token);
|
||||
console.log("REFRESH", refreshToken);
|
||||
|
||||
saveTokens(token, refreshToken);
|
||||
saveAuth(user, token);
|
||||
|
||||
navigate("/feed");
|
||||
|
||||
@@ -2,35 +2,56 @@ import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Client } from "@stomp/stompjs";
|
||||
import SockJS from "sockjs-client";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { getAccessToken, refreshAccessToken } from "../api/apiClient";
|
||||
|
||||
export function useChatSocket(chatId, onMessage) {
|
||||
const clientRef = useRef(null);
|
||||
const onMessageRef = useRef(onMessage);
|
||||
const [connected, setConnected] = useState(false);
|
||||
|
||||
// Держим актуальный колбэк в ref, чтобы не пересоздавать соединение
|
||||
// при каждом ре-рендере родителя из-за нового onMessage
|
||||
useEffect(() => {
|
||||
onMessageRef.current = onMessage;
|
||||
}, [onMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatId) return;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
const socket = new SockJS(`${API_URL}/ws?token=${token}`);
|
||||
|
||||
const client = new Client({
|
||||
webSocketFactory: () => socket,
|
||||
webSocketFactory: () => new SockJS(`${API_URL}/ws`),
|
||||
reconnectDelay: 3000,
|
||||
beforeConnect: async () => {
|
||||
// На каждую попытку коннекта — свежий токен из localStorage
|
||||
client.connectHeaders = {
|
||||
Authorization: `Bearer ${getAccessToken()}`,
|
||||
};
|
||||
},
|
||||
onConnect: () => {
|
||||
setConnected(true);
|
||||
client.subscribe(`/topic/chat/${chatId}`, (msg) => {
|
||||
const body = JSON.parse(msg.body);
|
||||
onMessage(body);
|
||||
onMessageRef.current(body);
|
||||
});
|
||||
},
|
||||
onDisconnect: () => setConnected(false),
|
||||
onStompError: async (frame) => {
|
||||
// Если сервер отклонил CONNECT из-за протухшего токена — пробуем обновить и переподключиться
|
||||
if (frame.headers?.message?.includes("Unauthorized")) {
|
||||
try {
|
||||
await refreshAccessToken();
|
||||
} catch {
|
||||
// refresh тоже не удался — дальше решает общий логин-флоу (redirect на /)
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
client.activate();
|
||||
clientRef.current = client;
|
||||
|
||||
return () => client.deactivate();
|
||||
}, [chatId, onMessage]);
|
||||
}, [chatId]);
|
||||
|
||||
const sendMessage = useCallback((content) => {
|
||||
if (!clientRef.current?.connected) return;
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { apiFetch } from "../api/apiClient";
|
||||
import { cachePostsFromChats, setPostForChat } from "../stores/chatPostStore";
|
||||
|
||||
function authHeaders() {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${localStorage.getItem("token")}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function useChats() {
|
||||
const [chats, setChats] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -16,7 +10,7 @@ export function useChats() {
|
||||
const fetchChats = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/v1/chats`, { headers: authHeaders() });
|
||||
const res = await apiFetch(`${API_URL}/api/v1/chats`);
|
||||
const data = await res.json();
|
||||
// API may return an array directly or a wrapper object like { items: [] } / { content: [] }
|
||||
const list = Array.isArray(data) ? data : (data.items ?? data.content ?? []);
|
||||
@@ -31,7 +25,7 @@ export function useChats() {
|
||||
const url = new URL(`${API_URL}/api/v1/chats/with/${otherUserId}`);
|
||||
if (postId) url.searchParams.set("postId", postId);
|
||||
|
||||
const res = await fetch(url, { method: "POST", headers: authHeaders() });
|
||||
const res = await apiFetch(url, { method: "POST" });
|
||||
if (!res.ok) throw new Error("Не удалось создать чат");
|
||||
const chat = await res.json();
|
||||
if (postInfo && chat.id) {
|
||||
@@ -41,7 +35,7 @@ export function useChats() {
|
||||
}, []);
|
||||
|
||||
const fetchMessages = useCallback(async (chatId) => {
|
||||
const res = await fetch(`${API_URL}/api/v1/chats/${chatId}/messages`, { headers: authHeaders() });
|
||||
const res = await apiFetch(`${API_URL}/api/v1/chats/${chatId}/messages`);
|
||||
const data = await res.json();
|
||||
// Normalize: array or wrapper object
|
||||
return Array.isArray(data) ? data : (data.items ?? data.content ?? []);
|
||||
@@ -49,9 +43,8 @@ export function useChats() {
|
||||
|
||||
const markAsRead = useCallback(async (chatId) => {
|
||||
try {
|
||||
await fetch(`${API_URL}/api/v1/chats/${chatId}/read`, {
|
||||
await apiFetch(`${API_URL}/api/v1/chats/${chatId}/read`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to mark as read:", e);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { apiFetch } from "../api/apiClient";
|
||||
|
||||
export function useCreatePost() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -11,18 +12,13 @@ export function useCreatePost() {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
const res = await fetch(`${API_URL}/api/v1/posts`, {
|
||||
const res = await apiFetch(`${API_URL}/api/v1/posts`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
// Content-Type НЕ указываем — браузер сам добавит multipart/form-data с boundary
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: formData, // ← передаём FormData
|
||||
});
|
||||
|
||||
const data = await res.json(); // ← объявляем ПОСЛЕ await res
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.message || "Error creating ad");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { apiFetch } from "../api/apiClient";
|
||||
|
||||
export function useDeleteChat() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -9,12 +10,8 @@ export function useDeleteChat() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
const res = await fetch(`${API_URL}/api/v1/chats/${chatId}`, {
|
||||
const res = await apiFetch(`${API_URL}/api/v1/chats/${chatId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Ошибка ${res.status}`);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { apiFetch } from "../api/apiClient";
|
||||
|
||||
/**
|
||||
* useDeletePost — хук для удаления объявления.
|
||||
@@ -15,12 +16,8 @@ export function useDeletePost() {
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
const res = await fetch(`${API_URL}/api/v1/posts/${postId}`, {
|
||||
const res = await apiFetch(`${API_URL}/api/v1/posts/${postId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { API_URL } from '../config/authConstants';
|
||||
import { apiFetch } from '../api/apiClient';
|
||||
|
||||
/**
|
||||
* Хук для работы с лайками (избранное).
|
||||
@@ -9,21 +10,12 @@ export function useFavorites() {
|
||||
/**
|
||||
* Переключает лайк на посте через API.
|
||||
* @param {number} postId - ID поста
|
||||
* @param {string} token - JWT токен авторизации
|
||||
* @returns {Promise<boolean>} - true если операция успешна
|
||||
*/
|
||||
const toggleFavorite = useCallback(async (postId, token) => {
|
||||
if (!token) {
|
||||
console.warn('Cannot toggle favorite: no token');
|
||||
return false;
|
||||
}
|
||||
|
||||
const toggleFavorite = useCallback(async (postId) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/v1/favorites/${postId}/toggle`, {
|
||||
const response = await apiFetch(`${API_URL}/api/v1/favorites/${postId}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -39,20 +31,11 @@ export function useFavorites() {
|
||||
|
||||
/**
|
||||
* Загружает список лайкнутых постов текущего пользователя.
|
||||
* @param {string} token - JWT токен авторизации
|
||||
* @returns {Promise<Array>} - массив ID лайкнутых постов
|
||||
*/
|
||||
const loadFavorites = useCallback(async (token) => {
|
||||
if (!token) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const loadFavorites = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/v1/favorites`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const response = await apiFetch(`${API_URL}/api/v1/favorites`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load favorites: ${response.status}`);
|
||||
|
||||
@@ -5,8 +5,17 @@ import { getImageUrl } from "../config/authConstants";
|
||||
const MAX_SIZE_MB = 5;
|
||||
export const ACCEPT = "image/jpeg,image/png,image/webp";
|
||||
|
||||
let idCounter = 0;
|
||||
const nextId = () => `img-${++idCounter}-${Date.now()}`;
|
||||
/**
|
||||
* Генератор уникального id для превью изображения.
|
||||
* crypto.randomUUID доступен во всех современных браузерах;
|
||||
* для страховки — fallback на timestamp + random.
|
||||
*/
|
||||
function generateImageId() {
|
||||
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
||||
return `img-${crypto.randomUUID()}`;
|
||||
}
|
||||
return `img-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
export function useImageUpload(existingUrls = []) {
|
||||
const [images, setImages] = useState([]);
|
||||
@@ -49,7 +58,7 @@ export function useImageUpload(existingUrls = []) {
|
||||
try {
|
||||
const compressedFiles = await Promise.all(files.map(f => compressImage(f)));
|
||||
const newImages = compressedFiles.map(file => ({
|
||||
id: nextId(),
|
||||
id: generateImageId(),
|
||||
file,
|
||||
preview: URL.createObjectURL(file),
|
||||
isExisting: false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { apiFetch } from "../api/apiClient";
|
||||
|
||||
/**
|
||||
* Универсальная мутация для создания и обновления объявления.
|
||||
@@ -9,7 +10,8 @@ import { API_URL } from "../config/authConstants";
|
||||
* id передан → PUT /api/v1/posts/:id (обновление)
|
||||
* id не передан → POST /api/v1/posts (создание)
|
||||
*
|
||||
* Автоматически добавляет заголовок Authorization с JWT-токеном.
|
||||
* Авторизация (Authorization header + авто-обновление токена при истечении)
|
||||
* обрабатывается внутри apiFetch.
|
||||
*
|
||||
* save() возвращает:
|
||||
* { data } — успешный ответ сервера
|
||||
@@ -29,21 +31,13 @@ export function usePostMutation() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
const url = id ? `${API_URL}/api/v1/posts/${id}` : `${API_URL}/api/v1/posts`;
|
||||
const method = id ? "PUT" : "POST";
|
||||
const res = await fetch(url, {
|
||||
const res = await apiFetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
throw new Error("Требуется авторизация. Войдите в аккаунт.");
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
let body;
|
||||
try { body = await res.json(); } catch { body = null; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { API_URL } from '../config/authConstants';
|
||||
import { apiFetch } from '../api/apiClient';
|
||||
|
||||
/**
|
||||
* Хук для изменения статуса поста (ACTIVE/CLOSED).
|
||||
@@ -9,21 +10,13 @@ export function usePostStatus() {
|
||||
/**
|
||||
* Изменяет статус поста на CLOSED (вещь найдена).
|
||||
* @param {number} postId - ID поста
|
||||
* @param {string} token - JWT токен авторизации
|
||||
* @returns {Promise<boolean>} - true если операция успешна
|
||||
*/
|
||||
const markAsFound = useCallback(async (postId, token) => {
|
||||
if (!token) {
|
||||
console.warn('Cannot mark post as found: no token');
|
||||
return false;
|
||||
}
|
||||
|
||||
const markAsFound = useCallback(async (postId) => {
|
||||
try {
|
||||
// Используем PATCH endpoint для обновления статуса
|
||||
const response = await fetch(`${API_URL}/api/v1/posts/${postId}/status`, {
|
||||
const response = await apiFetch(`${API_URL}/api/v1/posts/${postId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ status: 'CLOSED' }),
|
||||
@@ -43,20 +36,13 @@ export function usePostStatus() {
|
||||
/**
|
||||
* Возвращает статус поста в ACTIVE (если нужно reopen).
|
||||
* @param {number} postId - ID поста
|
||||
* @param {string} token - JWT токен авторизации
|
||||
* @returns {Promise<boolean>} - true если операция успешна
|
||||
*/
|
||||
const reopenPost = useCallback(async (postId, token) => {
|
||||
if (!token) {
|
||||
console.warn('Cannot reopen post: no token');
|
||||
return false;
|
||||
}
|
||||
|
||||
const reopenPost = useCallback(async (postId) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/v1/posts/${postId}/status`, {
|
||||
const response = await apiFetch(`${API_URL}/api/v1/posts/${postId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ status: 'ACTIVE' }),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
|
||||
import { saveTokens } from "../api/apiClient";
|
||||
|
||||
export function useCheckLogin(login) {
|
||||
const [checking, setChecking] = useState(false);
|
||||
@@ -15,7 +16,7 @@ export function useCheckLogin(login) {
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
setChecking(true);
|
||||
const res = await fetch(`${API_URL}/api/v1/auth/check-login?login=${login}`);
|
||||
const res = await fetch(`${API_URL}/api/v1/auth/check-login?login=${encodeURIComponent(login)}`);
|
||||
const json = await res.json();
|
||||
setExists(json.exists);
|
||||
} catch (e) {
|
||||
@@ -31,8 +32,8 @@ export function useCheckLogin(login) {
|
||||
return { checking, exists };
|
||||
}
|
||||
|
||||
|
||||
export function useRegister() {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
@@ -54,10 +55,9 @@ export function useRegister() {
|
||||
throw new Error(data.message || "Ошибка регистрации");
|
||||
}
|
||||
|
||||
saveTokens(data.token, data.refreshToken);
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
window.location.href = "/register";
|
||||
}, 2500);
|
||||
setTimeout(() => navigate("/feed"), 1500);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
|
||||
@@ -4,6 +4,7 @@ import SockJS from "sockjs-client";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { useUnread } from "../context/UnreadContext";
|
||||
import { getActiveChatId } from "../stores/activeChatStore";
|
||||
import { getAccessToken, refreshAccessToken } from "../api/apiClient";
|
||||
|
||||
export function useUnreadSocket(userId) {
|
||||
const { setChatUnread } = useUnread();
|
||||
@@ -12,12 +13,14 @@ export function useUnreadSocket(userId) {
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
const socket = new SockJS(`${API_URL}/ws?token=${token}`);
|
||||
|
||||
const client = new Client({
|
||||
webSocketFactory: () => socket,
|
||||
webSocketFactory: () => new SockJS(`${API_URL}/ws`),
|
||||
reconnectDelay: 3000,
|
||||
beforeConnect: () => {
|
||||
client.connectHeaders = {
|
||||
Authorization: `Bearer ${getAccessToken()}`,
|
||||
};
|
||||
},
|
||||
onConnect: () => {
|
||||
client.subscribe(`/topic/user/${userId}/unread`, (msg) => {
|
||||
const body = JSON.parse(msg.body);
|
||||
@@ -25,6 +28,15 @@ export function useUnreadSocket(userId) {
|
||||
setChatUnread(body.chatId, body.unreadCount);
|
||||
});
|
||||
},
|
||||
onStompError: async (frame) => {
|
||||
if (frame.headers?.message?.includes("Unauthorized")) {
|
||||
try {
|
||||
await refreshAccessToken();
|
||||
} catch {
|
||||
// refresh не удался — общий логин-флоу разберётся при следующем apiFetch
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
client.activate();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { apiFetch } from "../api/apiClient";
|
||||
|
||||
export function useUpdatePost() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -9,13 +10,8 @@ export function useUpdatePost() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
const res = await fetch(`${API_URL}/api/v1/posts/${id}`, {
|
||||
const res = await apiFetch(`${API_URL}/api/v1/posts/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: formData, // FormData
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {useState} from "react";
|
||||
import {API_URL} from "../config/authConstants";
|
||||
import {apiFetch} from "../api/apiClient";
|
||||
|
||||
/**
|
||||
* useUpdateProfile — хук для обновления профиля пользователя.
|
||||
@@ -14,8 +15,6 @@ export function useUpdateProfile() {
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
// Используем FormData — чтобы отправить файл аватара вместе с текстовыми полями
|
||||
const formData = new FormData();
|
||||
formData.append("id", userId);
|
||||
@@ -30,11 +29,8 @@ export function useUpdateProfile() {
|
||||
formData.append("avatar", profileData.avatar);
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_URL}/api/v1/users/${userId}`, {
|
||||
const res = await apiFetch(`${API_URL}/api/v1/users/${userId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
@@ -43,8 +39,7 @@ export function useUpdateProfile() {
|
||||
throw new Error(data.message || `Ошибка ${res.status}`);
|
||||
}
|
||||
|
||||
const updatedUser = await res.json();
|
||||
return updatedUser;
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
setError(err.message || "Failed to save profile");
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user