fixed lan and lon on post
Находка
Платформа для поиска потерянных вещей и питомцев. SPA на React 18 с JWT-аутентификацией и автоматическим обновлением токена, чатом на WebSocket (STOMP/SockJS), картами Яндекса, загрузкой изображений, админ-панелью рекламы и модерации объявлений, i18n (ru/en) и светлой/тёмной темой.
Содержание
- Быстрый старт
- Переменные окружения
- Стек и зависимости
- Скрипты
- Структура проекта
- Маршруты
- Ключевые компоненты
- Контексты
- Хуки
- API-эндпоинты
- WebSocket
- Админские эндпоинты
- Стили и темы
- Основные сценарии
- Известные проблемы и предупреждения
1. Быстрый старт
Требуется Node.js 18+ (проверено на Node 24).
npm install # зависимости (см. раздел о .npmrc)
npm start # dev-сервер на http://localhost:3000
npm run build # production-сборка в build/
Фронтенд ожидает работающий бэкенд (по умолчанию http://localhost:8080).
Если нужно обращаться к другому адресу — скопируйте .env.example в .env
и задайте REACT_APP_API_URL.
2. Переменные окружения
Файл .env не коммитится (см. .gitignore), шаблон — .env.example.
| Переменная | Обязательна | По умолчанию | Описание |
|---|---|---|---|
REACT_APP_API_URL |
да | http://localhost:8080 |
Базовый URL бэкенда |
REACT_APP_YANDEX_GEOCODER_KEY |
для карты | — | Яндекс.Геокодер: обратное геокодирование координат → адрес |
REACT_APP_YANDEX_MAPS_KEY |
для карты | — | Публичный ключ Яндекс.Карт, подставляется в public/index.html |
REACT_APP_SUPPORT_EMAIL |
нет | есть значение по умолчанию | Почта поддержки в модалке «Поддержка» |
Безопасность. Ключи Яндекса — публичные, но попадание реального
.envв git означает утечку в историю коммитов. Если файл уже коммитился, перевыпустите ключи: добавление.envв.gitignoreне удаляет его из истории.
3. Стек и зависимости
| Пакет | Назначение |
|---|---|
react / react-dom 18 |
UI |
react-router-dom 7 |
Роутинг (BrowserRouter) |
@stomp/stompjs + sockjs-client |
WebSocket (STOMP) для чата и непрочитанных |
framer-motion |
Анимации модалок и переходов |
react-hook-form + @hookform/resolvers + zod 4 |
Формы и их валидация |
i18next + react-i18next |
Локализация ru/en |
@emoji-mart/react + @emoji-mart/data + emoji-mart |
Эмодзи-пикер в чате |
react-scripts 5.0.1 |
Сборка/дев-сервер (CRA) |
Карты Яндекса подключаются напрямую через глобальный window.ymaps
(см. src/utils/ymapsApi.js), без обёртки — она ждёт готовности API
с таймаутом и отдаёт ошибку вместо вечного спиннера.
Dev-зависимости: typescript 5.9, @types/react 18, @types/react-dom,
@types/node, @testing-library/*.
.npmrc и конфликт peer-зависимостей
В репозитории лежит .npmrc с legacy-peer-deps=true. Причина: react-scripts@5.0.1
объявляет typescript опциональным peer-диапазоном ^3.2.1 || ^4, а проекту нужен
TypeScript 5 (на TS 4 не собираются @types/react 18.3 и zod 4). Конфликт формальный:
CRA типы не проверяет — их вырезает babel, а проверка запускается отдельно через
npm run typecheck.
Побочный эффект legacy-peer-deps: npm не ставит peer-зависимости автоматически,
поэтому emoji-mart объявлен в dependencies явно (его требует @emoji-mart/react).
4. Скрипты
| Скрипт | Что делает |
|---|---|
npm start |
Dev-сервер с hot reload |
npm run build |
Production-сборка в build/ |
npm test |
Jest в watch-режиме |
npm run test:ci |
Тесты один раз (--watchAll=false) — для CI |
npm run lint |
ESLint по src, --max-warnings=0 |
npm run typecheck |
tsc --noEmit по tsconfig.json |
Перед коммитом стоит прогнать все четыре проверки: lint, typecheck, test:ci, build.
5. Структура проекта
src/
├── index.js # Точка входа
├── App.js # Провайдеры + маршруты
├── i18n.js # Инициализация i18next, persist языка
├── legacy.d.ts # Типы ассетов (*.css, *.png, ...)
│
├── api/
│ ├── apiClient.jsx # apiFetch: Authorization + refresh токена по 401
│ ├── adminAdsApi.js # Кампании, реклама, фуллскрин-реклама
│ ├── adminContentApi.js # Админские объявления: список, статус, удаление
│ └── adsApi.js # Фуллскрин-реклама, пресigned-выгрузка видео
│
├── config/
│ ├── authConstants.js # API_URL
│ ├── feedConstants.js # Пункты меню, категории, пустая форма
│ └── landingConstants.js # Контент лендинга
│
├── context/
│ ├── AuthContext.jsx # user, token, isAuthenticated, isAdmin, login/logout
│ ├── ThemeContext.jsx # Светлая/тёмная тема
│ └── UnreadContext.jsx # Счётчики непрочитанных чатов
│
├── stores/
│ ├── activeChatStore.js # Активный чат
│ └── chatPostStore.js # Кэш «объявление ↔ чат»
│
├── hooks/ # см. раздел 9
│
├── utils/
│ ├── adSession.js # ID рекламной сессии (устойчив к запрету localStorage)
│ ├── campaignStatus.js # Статусы кампаний
│ ├── chatDisplay.jsx # Данные для списка чатов
│ ├── compressImage.js # Сжатие JPEG до 1200px
│ ├── formatPhone.js # Маска +7 (___) ___-__-__
│ ├── validatePostForm.js # Валидация формы объявления
│ ├── withAds.js # Рекламная обёртка
│ └── ymapsApi.js # Ожидание window.ymaps с таймаутом
│
├── pages/
│ ├── NakhodkaLanding.jsx # Лендинг «/»
│ ├── MainFeed.tsx # Лента объявлений — главная страница
│ ├── MapModal.jsx, MapView.jsx # Карта и полноэкранный режим
│ ├── ChatListPage.jsx, ChatPage.jsx
│ ├── ReviewsPage.jsx
│ ├── CreateUpdatePost.jsx # Создание/редактирование (в т.ч. adminMode)
│ ├── PostOnFeed.jsx # Карточка объявления
│ ├── RulesModal.jsx, SupportModal.jsx
│ └── AdminAdsPage.jsx # Админ-панель
│
├── components/
│ ├── feed/ FeedTopBar.tsx, FeedContent, FeedSearchBar, PullToRefresh, RewardPopup
│ ├── nav/ BottomNav.jsx, Sidebar.jsx
│ ├── layout/ AppTopBar.jsx
│ ├── chat/ ChatHeader, ChatInputBar, ChatListItem, MessageBubble
│ ├── reviews/ RatingSummary, ReviewCard, ReviewForm, StarRating
│ ├── profile/ Profile, ProfileAvatar, ProfileFormFields, ProfileModal, profileValidation
│ ├── admin/ PostsTab, AdvertisingCreate/EditModal, CampaignCreate/EditModal, ...
│ ├── landing/ Hero, Navbar, StepsSection, RadarBeacon, FinalCta, Footer, Icon
│ ├── icons/ NavIcons + Calendar/Close/Globe/Menu/Moon/Phone/Pin/Sun/Trash
│ └── … AuthModal, Toast, MapView, PostDetailModal, ImageViewer, AdCard, AdSlot, ...
│
├── styles/
│ ├── index.css # Точка входа CSS
│ ├── base.css # Переменные, сброс, keyframes
│ ├── layout.css, sidebar.css, card.css, modal.css, ui.css
│ └── chat.css, reviews.css, rules.css, admin.css, NakhodkaLanding.css
│
└── locales/
├── ru.json
└── en.json
6. Маршруты
| Путь | Компонент | Доступ |
|---|---|---|
/ |
NakhodkaLanding |
публичный лендинг |
/feed |
MainFeed |
лента (главная) |
/reviews |
ReviewsPage |
отзывы о площадке |
/chats |
ChatListPage |
список чатов |
/chats/:chatId |
ChatPage |
чат |
/admin/ads |
AdminAdsPage |
только isAdmin (иначе редирект на /feed) |
* |
NakhodkaLanding |
404-fallback |
Провайдеры: ThemeProvider → AuthProvider → BrowserRouter.
7. Ключевые компоненты
MainFeed.tsx
Главная страница. Собирает топбар, поиск, фильтры, ленту, сайдбары, карту,
модалки (объявление, профиль, вход, правила, поддержка, просмотр фото).
Делегирует загрузку и мутации хукам useFeedPosts, useCityFeed, useSearchPosts,
useMyPosts, useFeedActions, useModals. Ошибки удаления/отметки «найдено»
показываются через Toast.
PostOnFeed.jsx
Карточка объявления: фото, бейджи категории/типа/статуса, заголовок, описание, адрес, дата, телефон, кнопка карты и «Написать». Для владельца — редактирование, удаление, отметка «найдено».
CreateUpdatePost.jsx
Модалка создания/редактирования. Переключатель Потеря/Находка, заголовок,
описание, фото (сжатие на клиенте), категория, карта Яндекса с меткой и обратным
геокодированием, адрес, телефон с маской, чекбокс правил. Валидация —
validatePostForm. С пропом adminMode шлёт PUT /api/v1/admin/posts/{id}.
FeedContent.jsx / FeedTopBar.tsx / PullToRefresh.jsx
Лента, топбар со спрятанной панелью на скролле, pull-to-refresh с корректным cleanup класса активности.
MapView.jsx
Полноэкранная карта. Ждёт готовности API Яндекса, показывает плашку ошибки,
если он не загрузился. Первая загрузка маркеров идёт по фактическим границам
карты, а не по событию load (которого у ymaps.Map в 2.1 нет).
AdminAdsPage.jsx + components/admin/PostsTab.jsx
Админ-панель: вкладки «Рекламные кампании» и «Все объявления».
Кампании, реклама, загрузка видео (presigned → S3 → confirm), фуллскрин-реклама.
PostsTab — список всех объявлений с поиском, фильтрами, пагинацией, правкой,
закрытием/возвратом в ленту и удалением.
8. Контексты
AuthContext
{
user, // object | null
token, // string | null — JWT
isAuthenticated, // !!token
isAdmin, // user.role === "ADMIN" — только скрытие UI, не защита
login(user, token), // пишет в localStorage + state
logout(), // чистит localStorage + state
updateUser(userData), // обновляет пользователя
}
Хранится в localStorage: user (JSON), token, refreshToken.
Хук: useAuthContext().
ThemeContext
Светлая/тёмная тема; выбор сохраняется.
UnreadContext
Счётчики непрочитанных по чатам; сбрасываются при смене пользователя,
данные приходят из WS-подписки /topic/user/{userId}/unread.
9. Хуки
Авторизация и API
| Хук | Назначение |
|---|---|
apiClient.jsx → apiFetch |
fetch + Authorization + автоматический refresh по 401 (один refresh на параллельные запросы) |
useProfile.js |
Загрузка профиля |
useUpdateProfile.js |
Обновление профиля с аватаром |
Объявления
| Хук | Назначение |
|---|---|
useFeedPosts.jsx |
Лента с пагинацией, request-id guard, дедупликация |
useCityFeed.jsx |
Лента по городу |
useSearchPosts.js |
Поиск с дебаунсом |
useMyPosts.jsx |
Мои объявления |
useFeedActions.jsx |
Удаление и отметка «найдено» + error/clearError |
usePostMutation.js |
Создание/обновление (обычное и adminMode) |
useDeletePost.js, usePostStatus.js |
Точечные мутации |
useImageUpload.js |
Выбор, сжатие, превью изображения |
useModals.jsx |
Управление модалками |
useInfiniteScroll.jsx |
Бесконечная прокрутка с cleanup IntersectionObserver |
useCities.js, useClickOutside.jsx, useIsDesktop.js |
Вспомогательные |
Реклама
| Хук | Назначение |
|---|---|
useFullscreenAdTrigger.js |
Показ фуллскрин-рекламы |
useAdClickTracking.js, useAdImpressionTracking.js |
Клики/показы |
useTranscodingStatusPolling.js |
Статус транскодинга видео: сброс между роликами, fallback PROCESSING, лимит попыток |
Чат
| Хук | Назначение |
|---|---|
useStompClient.js |
Общий STOMP-клиент: reconnect, обработка close/ошибок, стабильные колбэки |
useChatSocket.js |
Подписка на чат, отправка сообщений |
useUnreadSocket.js |
Непрочитанные |
useChats.js |
Список чатов, создание, сообщения (request-id guard) |
useContactSeller.js |
Создание/получение чата с продавцом |
useDeleteChat.js |
Удаление чата |
Карта и локация
| Хук | Назначение |
|---|---|
useGeolocation.js |
Геолокация пользователя со статусами |
useYandexMap.js |
Инициализация карты, метка (drag), обратное геокодирование, ошибка загрузки API |
Прочее
| Хук | Назначение |
|---|---|
useReviews.jsx |
Отзывы: загрузка, отправка, средний рейтинг и распределение |
useLanguageToggle.js |
Переключение ru/en |
10. API-эндпоинты
Базовый URL — REACT_APP_API_URL. Защищённые эндпоинты вызываются через
apiFetch, который сам добавляет Authorization: Bearer <JWT> и обновляет
токен при 401.
Аутентификация
| Метод | Endpoint | Тело | Ответ |
|---|---|---|---|
POST |
/api/v1/auth/login |
{ login, password } |
{ token, refreshToken, user } |
POST |
/api/v1/auth/register |
{ name, login, password } |
{ token, refreshToken, user } |
POST |
/api/v1/auth/refresh |
{ refreshToken } |
{ token, refreshToken } |
Объявления
| Метод | Endpoint | Назначение |
|---|---|---|
GET |
/api/v1/posts?page=&size=&… |
Лента с пагинацией (content / items / массив) |
GET |
/api/v1/posts/search?… |
Поиск |
GET |
/api/v1/posts/cities |
Список городов |
GET |
/api/v1/posts/map?minLat&maxLat&minLng&maxLng&… |
Маркеры в границах карты |
GET |
/api/v1/posts/detail/:postId |
Карточка объявления |
GET |
/api/v1/posts/:userId |
Объявления пользователя |
POST |
/api/v1/posts |
Создать (FormData) |
PUT |
/api/v1/posts/:id |
Обновить (FormData) |
DELETE |
/api/v1/posts/:id |
Удалить |
PATCH |
/api/v1/posts/:postId/status |
Статус ACTIVE / CLOSED |
Профиль
| Метод | Endpoint | Назначение |
|---|---|---|
PUT |
/api/v1/users/:userId |
Обновить профиль (FormData) |
Чаты
| Метод | Endpoint | Назначение |
|---|---|---|
GET |
/api/v1/chats |
Список чатов |
POST |
/api/v1/chats/with/:otherUserId |
Создать/получить чат ({ postId }) |
GET |
/api/v1/chats/:chatId/messages |
Сообщения |
POST |
/api/v1/chats/:chatId/messages |
Отправить |
POST |
/api/v1/chats/:chatId/read |
Отметить прочитанным |
DELETE |
/api/v1/chats/:chatId |
Удалить чат |
Отзывы
| Метод | Endpoint | Тело |
|---|---|---|
GET |
/api/v1/reviews |
— |
POST |
/api/v1/reviews |
{ rating, text } |
Файлы и реклама
| Метод | Endpoint | Назначение |
|---|---|---|
GET |
/api/v1/files/:filename |
Изображение по имени |
GET |
/api/fullscreen-ad |
Текущая фуллскрин-реклама |
GET |
/api/ads/next |
Следующая реклама |
POST |
/api/ads/click, /api/ads/impression |
Трекинг |
Значения категорий в запросах — стабильные русские строки (Вещь, Питомец,
Другое): они хранятся в БД, поэтому не переводятся вместе с интерфейсом.
Локализованы только подписи (feedConstants.js → CATEGORIES).
11. WebSocket
STOMP поверх SockJS: ${REACT_APP_API_URL}/ws (токен передаётся при подключении).
| Назначение | Путь |
|---|---|
| Подписка на чат | /topic/chat/{chatId} |
| Подписка на непрочитанные | /topic/user/{userId}/unread |
| Отправка сообщения | /app/chat.send |
Общий клиент — useStompClient: единый STOMP-клиент с reconnect, обработкой
закрытия соединения и ошибок активации; колбэки хранятся в ref, поэтому
переподписка не пересоздаёт подключение.
12. Админские эндпоинты
Админ-панель живёт на /admin/ads (только для isAdmin, только десктоп) и
переключается вкладками:
/admin/ads— рекламные кампании;/admin/ads?tab=posts— «Все объявления».
Общие требования
- Префикс
/api/v1/admin, все запросы сAuthorization: Bearer <JWT>. - Роль
ADMINобязана проверяться на сервере по каждому эндпоинту. КлиентскийisAdmin— только скрытие UI; подделкаlocalStorageне должна давать доступ к чужим объявлениям и переписке. - Тело ошибок — JSON
{"message": "..."}; фронт показывает этот текст. - Пагинация как у
GET /api/v1/posts:{ "content": [...], "last": bool, "totalElements": N }. Фронт принимает иcontent, иitems, и голый массив.
Реклама (работает)
/api/v1/admin/campaigns, /api/v1/admin/ads, /api/v1/admin/fullscreen-ads.
Объявления
GET /api/v1/admin/posts
Параметры (все опциональны): page (с 0), size (по умолчанию 20, максимум 100),
search, type (LOSS/FOUND), status.
Видны все статусы, включая MODERATION и REJECTED. Сортировка по умолчанию —
новые сверху (createdAt DESC). search ищет без учёта регистра по названию,
описанию, городу, району и данным владельца (login, имя, фамилия).
Элемент content — то же, что у публичного объявления, плюс данные автора
(ownerId, ownerLogin, ownerName, ownerFirstName, ownerLastName).
Фронт читает автора с запасом (ownerLogin / authorLogin / userLogin и т. п.).
PUT /api/v1/admin/posts/{postId}
multipart/form-data, как у пользовательского PUT /api/v1/posts/{id}:
type, title, description, category, phone, reward, rewardText,
images, existingImages, latitude, longitude, address.
Ответ 200 — обновлённое объявление.
Отличия от пользовательской правки:
- Статус сохраняется — админ чинит объявление, не убирая его из ленты
(
MODERATIONотправил бы на повторную AI-модерацию,REJECTEDскрыл бы навсегда). Смена статуса — отдельнымPATCH. - Падение геокодинга не блокирует правку: город берётся из
address. - Изображения, которых нет в
existingImages, удаляются.
DELETE /api/v1/admin/posts/{postId}
Ответ 204 No Content. Вместе с объявлением чистятся изображения в хранилище,
записи из favorites и обнуляется post_id в чатах.
PATCH /api/v1/admin/posts/{postId}/status
Тело { "status": "CLOSED" } — закрыть («найдено»), { "status": "ACTIVE" } —
вернуть в ленту. Принимаются все PostStatus: ACTIVE, CLOSED, MODERATION,
REJECTED (фронт отдаёт первые два). Переход в CLOSED увеличивает posts_found.
Неизвестный статус — 400.
Чаты (вкладка не сделана)
Требуемые эндпоинты:
| Метод | Endpoint | Назначение |
|---|---|---|
GET |
/api/v1/admin/chats?page&size&search&postId |
Список чатов с превью |
GET |
/api/v1/admin/chats/{chatId}/messages |
Сообщения чата |
POST |
/api/v1/admin/chats/{chatId}/messages |
Написать в чат ({ "content": "…" }) |
DELETE |
/api/v1/admin/chats/{chatId} |
Удалить чат с перепиской |
Важно: senderId должен браться из JWT, а не из тела запроса, иначе админ
мог бы слать сообщения от имени пользователей. Авторизация — только ADMIN;
это осознанное расширение прав по сравнению с пользовательским чатом
(например, чтобы предупредить о мошенничестве).
Что сделано на клиенте
src/api/adminContentApi.js—getAdminPosts,deleteAdminPost,updateAdminPostStatus; пагинация принимаетcontent/items/ массив;typeнормализуется вloss/found.CreateUpdatePostс пропомadminMode→usePostMutationшлётPUT /api/v1/admin/posts/{id}(форма и валидация общие с пользовательской).src/components/admin/PostsTab.jsx— список, поиск с дебаунсом, фильтры, «Показать ещё», карточки с действиями.
Что осталось на клиенте
- Вкладка «Все чаты» (
ChatsTab.jsxне создана) — ждёт эндпоинтов чатов. - Проверка роли и скрытие UI:
AuthContext.isAdmin,AdminRouteвApp.js,isAdminв сайдбаре. Это удобство, не защита. - Админка не постит сообщения через STOMP: используется REST, чтобы реплика
не зависела от того, разрешена ли админу подписка
/topic/chat/{id}.
Статус бэкенда
Репозиторий бэкенда: D:\reFound\ReFound.
| Файл | Назначение |
|---|---|
controller/admin/PostAdminController.java |
Эндпоинты объявлений, у каждого @PreAuthorize("hasRole('ADMIN')") |
service/admin/AdminPostService.java |
Логика: список, правка, удаление, статус |
dto/admin/AdminPostResponse.java |
Объявление + данные владельца |
dto/admin/AdminPostStatusUpdateRequest.java |
{ "status": "..." } |
util/PostSpecification.java |
adminSearch — поиск по полям объявления и владельца |
repository/ImageRepository.java |
findByPostIdIn — картинки страницы одним запросом вместо N+1 |
repository/ChatRepository.java |
clearPostReference — обнуление post_id при удалении |
test/.../AdminPostServiceTest.java |
13 тестов: поиск, фильтры, статус, статистика, картинки, кэш |
Защита дублируется: SecurityConfig требует ADMIN на /api/v1/admin/**,
и методы контроллера дополнительно закрыты @PreAuthorize.
13. Стили и темы
Подключение через src/styles/index.css:
index.css → base.css → layout.css → sidebar.css → card.css → modal.css → ui.css
Дополнительно подключаются в компонентах: chat.css (App.js),
reviews.css, rules.css, admin.css, NakhodkaLanding.css.
CSS-переменные в base.css:
| Переменная | Значение | Описание |
|---|---|---|
--teal |
#2de0c8 |
Основной акцент |
--teal2 |
#1fc9b0 |
Тёмный оттенок |
--border |
rgba(255,255,255,0.09) |
Границы |
--muted / --muted2 |
rgba(255,255,255,.38) / .15 |
Второстепенный текст |
--font |
Nunito, sans-serif |
Шрифт |
--err |
#ff6b6b |
Ошибка |
Тёмная тема — градиентные фоны, «стекло» (backdrop-filter), акцент #2de0c8;
светлая переключается через ThemeContext.
Адаптивность:
| Брейкпоинт | Что меняется |
|---|---|
≤ 900px |
Правый сайдбар скрывается, навигация внизу |
≤ 768px |
Компактные отступы и кнопки |
≤ 600px |
Модалки на всю ширину |
14. Основные сценарии
Аутентификация. Гость нажимает «Разместить», «Мои объявления» или аватар →
открывается AuthModal → POST /api/v1/auth/login → AuthContext.login()
сохраняет user, token, refreshToken в localStorage. Просрочка access-токена
обрабатывается прозрачно: apiFetch при 401 обновляет токен и повторяет запрос;
при неудаче чистит сессию.
Создание объявления. Авторизованный пользователь → CreateUpdatePost →
заполняет форму → клиентское сжатие фото → POST /api/v1/posts (FormData) →
лента обновляется.
Поиск и чат. «Написать» → POST /api/v1/chats/with/:otherUserId →
переход на /chats/:chatId → подписка на /topic/chat/{chatId} →
сообщения в реальном времени.
Карта. useYandexMap ждёт window.ymaps (с таймаутом и ошибкой вместо
вечного спиннера), клик по карте ставит метку, drag метки и клик переводят
координаты в адрес через Яндекс.Геокодер; запрос на геокодинг защищён
request-id от гонок.
Профиль. Аватар в топбаре → ProfileModal → PUT /api/v1/users/:id →
данные обновляются в AuthContext и localStorage; «Выйти» — очистка сессии.
15. Известные проблемы и предупреждения
fs.F_OK is deprecatedпри сборке — предупреждение Node 24 из внутренностейreact-scripts@5. Не влияет на результат; уходит при переходе на CRA 6 / Vite.npm auditпоказывает уязвимости в транзитивных зависимостях CRA (webpack-dev-server и др.). Применятьnpm audit fix --forceнельзя: это ломающие обновления в пределахreact-scripts@5.- Ключи Яндекса попадали в историю git — их нужно перевыпустить.
- Клиентская проверка роли (
isAdmin,AdminRoute) — только скрытие UI. Реальная авторизация обязана быть на сервере по каждому эндпоинту. - Известная проблема бэкенда:
GlobalExceptionHandlerловитExceptionбез исключений, поэтому битый JSON или неверный тип параметра дают500, а не400. Из-за этогоtypeиstatusв админских эндпоинтах принимаются строками и разбираются вручную.