added security and fix edit profile
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
import RegisterPage from './components/Authentication';
|
||||
import MainFeed from './components/MainFeed'; // ← добавь импорт
|
||||
import MainFeed from './components/MainFeed';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/feed" replace />} />
|
||||
@@ -12,6 +14,7 @@ function App() {
|
||||
<Route path="/feed" element={<MainFeed />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import "../styles/index.css";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import Sidebar from "./Sidebar";
|
||||
@@ -7,6 +7,8 @@ import PostOnFeed from "./PostOnFeed";
|
||||
import CreateUpdatePost from "./CreateUpdatePost";
|
||||
import MenuIcon from "./icons/MenuIcon";
|
||||
import { ProfileModal } from "./profile";
|
||||
import AuthModal from "./AuthModal";
|
||||
import { useAuthContext } from "../context/AuthContext";
|
||||
|
||||
/**
|
||||
* MainFeed — корневой компонент приложения.
|
||||
@@ -14,6 +16,8 @@ import { ProfileModal } from "./profile";
|
||||
* остальные компоненты получают данные через пропсы.
|
||||
*/
|
||||
export default function MainFeed() {
|
||||
const { user, token, isAuthenticated, logout } = useAuthContext();
|
||||
|
||||
// Открыт ли левый сайдбар (мобильный слайд-аут)
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
// Активный пункт навигации в сайдбаре
|
||||
@@ -26,21 +30,32 @@ export default function MainFeed() {
|
||||
const [editAd, setEditAd] = useState(null);
|
||||
// Открыто ли модальное окно редактирования профиля
|
||||
const [profileModalOpen, setProfileModalOpen] = useState(false);
|
||||
// Данные профиля пользователя
|
||||
const [profileData, setProfileData] = useState({
|
||||
firstName: "Иван",
|
||||
lastName: "Иванов",
|
||||
email: "ivan@example.com",
|
||||
phone: "+7 (999) 123-45-67",
|
||||
});
|
||||
// Массив объявлений из API
|
||||
// Открыто ли модальное окно аутентификации (login / register)
|
||||
const [authModalOpen, setAuthModalOpen] = useState(false);
|
||||
// Массив объявлений из API (основная лента)
|
||||
const [ads, setAds] = useState([]);
|
||||
// Объявления текущего пользователя (загружаются отдельно по userId)
|
||||
const [myAds, setMyAds] = useState([]);
|
||||
// Set id объявлений, которые пользователь лайкнул (только локально)
|
||||
const [liked, setLiked] = useState(new Set());
|
||||
// Состояния загрузки и ошибки для ленты
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// Данные профиля — из контекста авторизации (или пустой объект для гостя)
|
||||
const profileData = useMemo(() => {
|
||||
if (!user) return {};
|
||||
return {
|
||||
login: user.login || "",
|
||||
firstName: user.name || user.firstName || "",
|
||||
lastName: user.lastName || "",
|
||||
email: user.email || "",
|
||||
phone: user.phone || "",
|
||||
avatar: user.avatar || null,
|
||||
id: user.id,
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
/** Загружает ленту объявлений с бэкенда. */
|
||||
const loadFeed = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -67,6 +82,33 @@ export default function MainFeed() {
|
||||
// Первичная загрузка при монтировании компонента
|
||||
useEffect(() => { loadFeed(); }, [loadFeed]);
|
||||
|
||||
/** Загружает объявления текущего пользователя по userId с бэкенда. */
|
||||
const loadMyPosts = useCallback(async () => {
|
||||
if (!user?.id) return;
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/v1/posts/${user.id}`);
|
||||
if (res.status === 404) {
|
||||
// У пользователя пока нет объявлений — это нормальная ситуация
|
||||
setMyAds([]);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error(`Ошибка ${res.status}`);
|
||||
const data = await res.json();
|
||||
const posts = Array.isArray(data) ? data : (data.items ?? []);
|
||||
const normalizedPosts = posts.map(ad => ({
|
||||
...ad,
|
||||
type: ad.type ? ad.type.toLowerCase() : "loss"
|
||||
}));
|
||||
setMyAds(normalizedPosts);
|
||||
} catch (err) {
|
||||
setError(err.message || "Не удалось загрузить ваши объявления");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
/**
|
||||
* Переключает лайк на объявлении.
|
||||
* Состояние хранится только на клиенте — без запроса к API.
|
||||
@@ -81,20 +123,58 @@ export default function MainFeed() {
|
||||
|
||||
// После создания или редактирования объявления перезагружаем ленту,
|
||||
// чтобы получить актуальные данные (включая изображения) из БД
|
||||
const handleNewAd = useCallback(() => loadFeed(), [loadFeed]);
|
||||
const handleEditSave = useCallback(() => loadFeed(), [loadFeed]);
|
||||
const handleNewAd = useCallback(() => { loadFeed(); loadMyPosts(); }, [loadFeed, loadMyPosts]);
|
||||
const handleEditSave = useCallback(() => { loadFeed(); loadMyPosts(); }, [loadFeed, loadMyPosts]);
|
||||
|
||||
/**
|
||||
* Обработчик нажатия «Разместить объявление».
|
||||
* Если пользователь не авторизован — открывает модалку входа.
|
||||
*/
|
||||
const handleCreatePost = useCallback(() => {
|
||||
if (!isAuthenticated) {
|
||||
setAuthModalOpen(true);
|
||||
} else {
|
||||
setModalOpen(true);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
/**
|
||||
* Обработчик редактирования — разрешён только автору объявления.
|
||||
*/
|
||||
const handleEdit = useCallback((ad) => {
|
||||
if (!isAuthenticated) {
|
||||
setAuthModalOpen(true);
|
||||
} else {
|
||||
setEditAd(ad);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Когда переключаемся на «Мои объявления» — подгружаем с бэкенда по userId
|
||||
useEffect(() => {
|
||||
if (activeNav === "Мои объявления" && isAuthenticated && user?.id) {
|
||||
loadMyPosts();
|
||||
}
|
||||
}, [activeNav, isAuthenticated, user?.id, loadMyPosts]);
|
||||
|
||||
// Фильтруем объявления в зависимости от выбранной вкладки и фильтра типа
|
||||
const filteredAds = (() => {
|
||||
let result = activeNav === "Главная"
|
||||
? ads
|
||||
: ads.filter(ad =>
|
||||
let result;
|
||||
|
||||
if (activeNav === "Мои объявления") {
|
||||
// Используем объявления, загруженные с бэкенда по userId
|
||||
result = myAds;
|
||||
} else if (activeNav === "Главная") {
|
||||
result = ads;
|
||||
} else {
|
||||
result = ads.filter(ad =>
|
||||
activeNav === "Вещи"
|
||||
? ["Вещь", "Другое"].includes(ad.category)
|
||||
: activeNav === "Питомцы"
|
||||
? ad.category === "Питомец"
|
||||
: false
|
||||
);
|
||||
}
|
||||
|
||||
// Применяем фильтр по типу объявления
|
||||
if (typeFilter) {
|
||||
result = result.filter(ad => ad.type === typeFilter);
|
||||
@@ -130,13 +210,17 @@ export default function MainFeed() {
|
||||
);
|
||||
}
|
||||
if (!filteredAds.length) {
|
||||
const emptyMsg = activeNav === "Мои объявления"
|
||||
? { title: "У вас нет объявлений", sub: "Создайте первое — нажмите кнопку выше" }
|
||||
: { title: "Объявлений пока нет", sub: "Будьте первым — разместите объявление<br />о потере или находке" };
|
||||
|
||||
return (
|
||||
<div className="f-center">
|
||||
<div className="f-center-icon">🔍</div>
|
||||
<div className="f-center-title">Объявлений пока нет</div>
|
||||
<div className="f-center-sub">
|
||||
Будьте первым — разместите объявление<br />о потере или находке
|
||||
</div>
|
||||
<div className="f-center-title">{emptyMsg.title}</div>
|
||||
<div className="f-center-sub"
|
||||
dangerouslySetInnerHTML={{ __html: emptyMsg.sub }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -144,9 +228,11 @@ export default function MainFeed() {
|
||||
<PostOnFeed
|
||||
key={ad.id}
|
||||
ad={ad}
|
||||
liked={liked.has(ad.id)} // передаём локальное состояние лайка
|
||||
onLike={toggleLike} // колбэк для переключения лайка
|
||||
onEdit={setEditAd} // колбэк открывает модалку редактирования
|
||||
liked={liked.has(ad.id)}
|
||||
onLike={toggleLike}
|
||||
onEdit={handleEdit}
|
||||
// На вкладке «Мои объявления» все посты принадлежат пользователю
|
||||
isOwner={activeNav === "Мои объявления" || (isAuthenticated && ad.userId === user?.id)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
@@ -159,7 +245,14 @@ export default function MainFeed() {
|
||||
activeNav={activeNav}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
onNavClick={setActiveNav}
|
||||
onEditProfile={() => setProfileModalOpen(true)}
|
||||
onEditProfile={() => {
|
||||
if (!isAuthenticated) { setAuthModalOpen(true); return; }
|
||||
setProfileModalOpen(true);
|
||||
}}
|
||||
profileData={profileData}
|
||||
isAuthenticated={isAuthenticated}
|
||||
onLogout={logout}
|
||||
onLoginClick={() => setAuthModalOpen(true)}
|
||||
/>
|
||||
|
||||
<div className="f-main">
|
||||
@@ -169,7 +262,7 @@ export default function MainFeed() {
|
||||
<MenuIcon />
|
||||
</button>
|
||||
<div className="f-topbar-center">
|
||||
<button className="f-post-btn" onClick={() => setModalOpen(true)}>
|
||||
<button className="f-post-btn" onClick={handleCreatePost}>
|
||||
<span style={{ fontSize: "1.15rem", fontWeight: 900, lineHeight: 1 }}>+</span>
|
||||
Разместить объявление
|
||||
</button>
|
||||
@@ -210,8 +303,8 @@ export default function MainFeed() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Модалка создания нового объявления */}
|
||||
{modalOpen && (
|
||||
{/* Модалка создания нового объявления (только для авторизованных) */}
|
||||
{modalOpen && isAuthenticated && (
|
||||
<CreateUpdatePost
|
||||
onClose={() => setModalOpen(false)}
|
||||
onSuccess={handleNewAd}
|
||||
@@ -219,7 +312,7 @@ export default function MainFeed() {
|
||||
)}
|
||||
|
||||
{/* Модалка редактирования — открывается когда editAd !== null */}
|
||||
{editAd && (
|
||||
{editAd && isAuthenticated && (
|
||||
<CreateUpdatePost
|
||||
initialData={editAd}
|
||||
onClose={() => setEditAd(null)}
|
||||
@@ -228,12 +321,19 @@ export default function MainFeed() {
|
||||
)}
|
||||
|
||||
{/* Модальное окно редактирования профиля */}
|
||||
{profileModalOpen && (
|
||||
{profileModalOpen && isAuthenticated && (
|
||||
<ProfileModal
|
||||
onClose={() => setProfileModalOpen(false)}
|
||||
initialData={profileData}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Модальное окно входа / регистрации */}
|
||||
{authModalOpen && (
|
||||
<AuthModal
|
||||
onClose={() => setAuthModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import MapIcon from "./icons/MapIcon";
|
||||
import HeartIcon from "./icons/HeartIcon";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
|
||||
export default function PostOnFeed({ ad, liked, onLike, onEdit }) {
|
||||
export default function PostOnFeed({ ad, liked, onLike, onEdit, isOwner }) {
|
||||
const imageUrl = ad.images?.[0] ? `${API_URL}${ad.images[0]}` : null;
|
||||
|
||||
// ✅ Формируем строку адреса из city + district
|
||||
@@ -53,7 +53,7 @@ export default function PostOnFeed({ ad, liked, onLike, onEdit }) {
|
||||
)}
|
||||
</div>
|
||||
<div className="f-card-foot-right">
|
||||
{onEdit && (
|
||||
{onEdit && isOwner && (
|
||||
<button className="f-like-btn"
|
||||
onClick={e => { e.stopPropagation(); onEdit(ad); }}>
|
||||
✏️
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import LogoIcon from "./icons/LogoIcon";
|
||||
import React, { useState } from "react";
|
||||
import {NAV_BOTTOM, NAV_ITEMS} from "../config/feedConstants";
|
||||
import {NavIcons} from "./icons/NavIcons";
|
||||
import Profile from "./profile/Profile";
|
||||
|
||||
export default function Sidebar({ open, activeNav, onClose, onNavClick, onEditProfile }) {
|
||||
const [profileData] = useState({
|
||||
firstName: "Иван",
|
||||
lastName: "Иванов",
|
||||
email: "ivan@example.com",
|
||||
phone: "+7 (999) 123-45-67",
|
||||
});
|
||||
|
||||
export default function Sidebar({
|
||||
open,
|
||||
activeNav,
|
||||
onClose,
|
||||
onNavClick,
|
||||
onEditProfile,
|
||||
profileData = {},
|
||||
isAuthenticated,
|
||||
onLogout,
|
||||
onLoginClick,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className={`f-overlay${open ? " show" : ""}`} onClick={onClose} />
|
||||
@@ -23,11 +25,20 @@ export default function Sidebar({ open, activeNav, onClose, onNavClick, onEditPr
|
||||
<nav className="f-sb-nav">
|
||||
{NAV_ITEMS.map(({ icon, label }) => {
|
||||
const Ic = NavIcons[icon];
|
||||
const handleClick = () => {
|
||||
// «Мои объявления» и «Чат» требуют авторизации
|
||||
if (!isAuthenticated && (label === "Мои объявления" || label === "Чат")) {
|
||||
onLoginClick();
|
||||
} else {
|
||||
onNavClick(label);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={label}
|
||||
className={`f-sb-item${activeNav === label ? " active" : ""}`}
|
||||
onClick={() => { onNavClick(label); onClose(); }}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Ic /> {label}
|
||||
</div>
|
||||
@@ -48,7 +59,27 @@ export default function Sidebar({ open, activeNav, onClose, onNavClick, onEditPr
|
||||
);
|
||||
})}
|
||||
<div className="f-sb-divider" />
|
||||
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Profile data={profileData} onEdit={onEditProfile} />
|
||||
<div
|
||||
className="f-sb-item"
|
||||
onClick={() => { onLogout(); onClose(); }}
|
||||
style={{ color: "rgba(255,107,107,.7)", marginTop: 8 }}
|
||||
>
|
||||
<span style={{ fontSize: "1.1rem" }}>🚪</span> Выйти
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="f-sb-item"
|
||||
onClick={() => { onLoginClick(); onClose(); }}
|
||||
style={{ color: "#2de0c8" }}
|
||||
>
|
||||
<span style={{ fontSize: "1.1rem" }}>🔑</span> Войти
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -60,6 +60,19 @@ function ImageUpload({
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Login field ─────────────────────────────────────────────────────────────
|
||||
function LoginField({ form, errors, onChange }) {
|
||||
return (
|
||||
<InputField
|
||||
label="Логин"
|
||||
placeholder="username"
|
||||
value={form.login}
|
||||
error={errors.login}
|
||||
onChange={(e) => onChange("login", e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Name fields row ─────────────────────────────────────────────────────────
|
||||
function NameFieldsRow({ form, errors, onChange }) {
|
||||
return (
|
||||
@@ -134,4 +147,4 @@ function ModalHeader({ onClose }) {
|
||||
);
|
||||
}
|
||||
|
||||
export { InputField, ImageUpload, NameFieldsRow, ContactFields, SubmitButton, ModalHeader };
|
||||
export { InputField, ImageUpload, LoginField, NameFieldsRow, ContactFields, SubmitButton, ModalHeader };
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { formatPhone } from "../../utils/formatPhone";
|
||||
import { useImageUpload } from "../../hooks/useImageUpload";
|
||||
import { useUpdateProfile } from "../../hooks/useUpdateProfile";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { validateProfileForm, initializeFormState } from "./profileValidation";
|
||||
import {
|
||||
ImageUpload,
|
||||
LoginField,
|
||||
NameFieldsRow,
|
||||
ContactFields,
|
||||
SubmitButton,
|
||||
@@ -11,6 +14,8 @@ import {
|
||||
} from "./ProfileFormFields";
|
||||
|
||||
export default function ProfileModal({ onClose, initialData = {} }) {
|
||||
const { user, updateUser } = useAuthContext();
|
||||
const { updateProfile, loading: saving, error: saveError } = useUpdateProfile();
|
||||
const [form, setForm] = useState(initializeFormState(initialData));
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
@@ -47,15 +52,20 @@ export default function ProfileModal({ onClose, initialData = {} }) {
|
||||
}
|
||||
|
||||
const profileData = {
|
||||
firstName: form.firstName.trim(),
|
||||
lastName: form.lastName.trim(),
|
||||
name: form.firstName.trim(),
|
||||
login: form.login.trim(),
|
||||
phone: form.phone.trim(),
|
||||
email: form.email.trim(),
|
||||
avatar: imageFile || existingImageUrl,
|
||||
};
|
||||
|
||||
console.log("Profile data to save:", profileData);
|
||||
const updatedUser = await updateProfile(user?.id, profileData);
|
||||
|
||||
if (updatedUser) {
|
||||
// Обновляем данные пользователя в контексте и localStorage
|
||||
updateUser(updatedUser);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -72,6 +82,12 @@ export default function ProfileModal({ onClose, initialData = {} }) {
|
||||
onRemove={removeImage}
|
||||
/>
|
||||
|
||||
<LoginField
|
||||
form={form}
|
||||
errors={errors}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
|
||||
<NameFieldsRow
|
||||
form={form}
|
||||
errors={errors}
|
||||
@@ -85,7 +101,9 @@ export default function ProfileModal({ onClose, initialData = {} }) {
|
||||
onPhoneChange={handlePhoneChange}
|
||||
/>
|
||||
|
||||
<SubmitButton loading={compressing} onSubmit={handleSubmit} />
|
||||
{saveError && <p className="f-err-msg" style={{ marginBottom: 10 }}>{saveError}</p>}
|
||||
|
||||
<SubmitButton loading={compressing || saving} onSubmit={handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ export function validateProfileForm(form) {
|
||||
// ─── Form state initialization ──────────────────────────────────────────────
|
||||
export function initializeFormState(initialData = {}) {
|
||||
return {
|
||||
login: initialData.login || "",
|
||||
firstName: initialData.firstName || "",
|
||||
lastName: initialData.lastName || "",
|
||||
phone: initialData.phone || "",
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import { useAuthContext } from "../context/AuthContext";
|
||||
|
||||
export function useAuth() {
|
||||
const navigate = useNavigate();
|
||||
const { login: saveAuth } = useAuthContext();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const login = async (login, password) => {
|
||||
if (!login || !password) { setError("Введи login и пароль"); return; }
|
||||
if (!login || !password) { setError("Введите логин и пароль"); return; }
|
||||
setLoading(true); setError("");
|
||||
|
||||
try {
|
||||
@@ -20,13 +22,15 @@ export function useAuth() {
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.message || "Неверный login или пароль");
|
||||
throw new Error(data.message || "Неверный логин или пароль");
|
||||
}
|
||||
|
||||
const { token, user } = await res.json();
|
||||
localStorage.setItem("token", token);
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
navigate("/dashboard");
|
||||
|
||||
// Сохраняем в глобальный контекст + localStorage
|
||||
saveAuth(user, token);
|
||||
|
||||
navigate("/feed");
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -34,6 +38,5 @@ export function useAuth() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return { login, loading, error };
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import { API_URL } from "../config/authConstants";
|
||||
* id передан → PUT /api/v1/posts/:id (обновление)
|
||||
* id не передан → POST /api/v1/posts (создание)
|
||||
*
|
||||
* Автоматически добавляет заголовок Authorization с JWT-токеном.
|
||||
*
|
||||
* @returns {{
|
||||
* save: (id: string|null, data: FormData) => Promise<object|null>,
|
||||
* loading: boolean,
|
||||
@@ -28,9 +30,21 @@ 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, { method, body: data });
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
throw new Error("Требуется авторизация. Войдите в аккаунт.");
|
||||
}
|
||||
|
||||
if (!res.ok) throw new Error(`Ошибка сервера: ${res.status}`);
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user