diff --git a/src/api/apiClient.jsx b/src/api/apiClient.jsx new file mode 100644 index 0000000..c5e9741 --- /dev/null +++ b/src/api/apiClient.jsx @@ -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 }; \ No newline at end of file diff --git a/src/components/AuthModal.jsx b/src/components/AuthModal.jsx index e05828f..288034a 100644 --- a/src/components/AuthModal.jsx +++ b/src/components/AuthModal.jsx @@ -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) { diff --git a/src/hooks/useAuth.js b/src/hooks/useAuth.js index 4584ab1..1c54c7f 100644 --- a/src/hooks/useAuth.js +++ b/src/hooks/useAuth.js @@ -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"); @@ -39,4 +47,4 @@ export function useAuth() { }; return { login, loading, error }; -} +} \ No newline at end of file diff --git a/src/hooks/useChatSocket.js b/src/hooks/useChatSocket.js index 4b74dcd..38dc88f 100644 --- a/src/hooks/useChatSocket.js +++ b/src/hooks/useChatSocket.js @@ -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; @@ -41,4 +62,4 @@ export function useChatSocket(chatId, onMessage) { }, [chatId]); return { connected, sendMessage }; -} +} \ No newline at end of file diff --git a/src/hooks/useChats.js b/src/hooks/useChats.js index 737027e..36bb10a 100644 --- a/src/hooks/useChats.js +++ b/src/hooks/useChats.js @@ -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); @@ -59,4 +52,4 @@ export function useChats() { }, []); return { chats, loading, fetchChats, createOrGetChat, fetchMessages, markAsRead }; -} +} \ No newline at end of file diff --git a/src/hooks/useCreatePost.js b/src/hooks/useCreatePost.js index c5ed1e2..ed4354f 100644 --- a/src/hooks/useCreatePost.js +++ b/src/hooks/useCreatePost.js @@ -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}`, - }, + // Content-Type НЕ указываем — браузер сам добавит multipart/form-data с boundary 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"); diff --git a/src/hooks/useDeleteChat.js b/src/hooks/useDeleteChat.js index 3d1bcf1..ec1e618 100644 --- a/src/hooks/useDeleteChat.js +++ b/src/hooks/useDeleteChat.js @@ -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}`); @@ -29,4 +26,4 @@ export function useDeleteChat() { }, []); return { deleteChat, loading, error }; -} +} \ No newline at end of file diff --git a/src/hooks/useDeletePost.js b/src/hooks/useDeletePost.js index 5566ee4..5a8667d 100644 --- a/src/hooks/useDeletePost.js +++ b/src/hooks/useDeletePost.js @@ -1,9 +1,10 @@ import { useState, useCallback } from "react"; import { API_URL } from "../config/authConstants"; +import { apiFetch } from "../api/apiClient"; /** * useDeletePost — хук для удаления объявления. - * + * * @returns {Object} { deletePost, loading, error } */ export function useDeletePost() { @@ -13,14 +14,10 @@ export function useDeletePost() { const deletePost = useCallback(async (postId) => { setLoading(true); 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) { @@ -37,4 +34,4 @@ export function useDeletePost() { }, []); return { deletePost, loading, error }; -} +} \ No newline at end of file diff --git a/src/hooks/useFavorites.js b/src/hooks/useFavorites.js index e7a02b0..ca4fd01 100644 --- a/src/hooks/useFavorites.js +++ b/src/hooks/useFavorites.js @@ -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} - 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} - массив 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}`); @@ -71,4 +54,4 @@ export function useFavorites() { toggleFavorite, loadFavorites, }; -} +} \ No newline at end of file diff --git a/src/hooks/useImageUpload.js b/src/hooks/useImageUpload.js index 0f458c6..904d7ea 100644 --- a/src/hooks/useImageUpload.js +++ b/src/hooks/useImageUpload.js @@ -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, @@ -103,4 +112,4 @@ export function useImageUpload(existingUrls = []) { getFilesForUpload, getExistingUrlsToKeep, }; -} +} \ No newline at end of file diff --git a/src/hooks/usePostMutation.js b/src/hooks/usePostMutation.js index 2828386..b9a63be 100644 --- a/src/hooks/usePostMutation.js +++ b/src/hooks/usePostMutation.js @@ -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; } @@ -65,4 +59,4 @@ export function usePostMutation() { }; return { save, loading, error }; -} +} \ No newline at end of file diff --git a/src/hooks/usePostStatus.js b/src/hooks/usePostStatus.js index deaeba0..9611a61 100644 --- a/src/hooks/usePostStatus.js +++ b/src/hooks/usePostStatus.js @@ -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} - 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} - 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' }), @@ -77,4 +63,4 @@ export function usePostStatus() { markAsFound, reopenPost, }; -} +} \ No newline at end of file diff --git a/src/hooks/useRegister.js b/src/hooks/useRegister.js index 9813285..6df70e2 100644 --- a/src/hooks/useRegister.js +++ b/src/hooks/useRegister.js @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; -import {API_URL} from "../config/authConstants"; - +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 { diff --git a/src/hooks/useUnreadSocket.js b/src/hooks/useUnreadSocket.js index 8ac17f1..19b41c9 100644 --- a/src/hooks/useUnreadSocket.js +++ b/src/hooks/useUnreadSocket.js @@ -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(); @@ -32,4 +44,4 @@ export function useUnreadSocket(userId) { return () => client.deactivate(); }, [userId, setChatUnread]); -} +} \ No newline at end of file diff --git a/src/hooks/useUpdatePost.js b/src/hooks/useUpdatePost.js index e99e5f7..855bfc6 100644 --- a/src/hooks/useUpdatePost.js +++ b/src/hooks/useUpdatePost.js @@ -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 }); diff --git a/src/hooks/useUpdateProfile.js b/src/hooks/useUpdateProfile.js index 6f06253..d166d4d 100644 --- a/src/hooks/useUpdateProfile.js +++ b/src/hooks/useUpdateProfile.js @@ -1,5 +1,6 @@ -import { useState } from "react"; -import { API_URL } from "../config/authConstants"; +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; @@ -54,4 +49,4 @@ export function useUpdateProfile() { }; return { updateProfile, loading, error }; -} +} \ No newline at end of file