diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ac64bbf..066e6f6 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -56,8 +56,12 @@ jobs: fetch-depth: 0 - name: Install trufflehog run: | + # Версия ЗАПИНЕНА: 2026-06-02 latest-тег trufflehog (3.95.4) указывал + # на релиз без выложенного бинарника → install давал HTTP 404 и валил + # secret-scan у всех PR. Пин на стабильную версию убирает зависимость + # от свежести релизов upstream. curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh \ - | sh -s -- -b /usr/local/bin + | sh -s -- -b /usr/local/bin v3.90.5 - name: Run trufflehog run: | trufflehog git "file://$(pwd)" \ diff --git a/src/api/Kubikon3DService.js b/src/api/Kubikon3DService.js index 144d4b2..f450b6a 100644 --- a/src/api/Kubikon3DService.js +++ b/src/api/Kubikon3DService.js @@ -3,7 +3,7 @@ * Бэкенд: storys-микросервис, префикс /kubikon3d/... */ import axios from 'axios'; -import { STORYS_addres } from './API'; +import { STORYS_addres, USER_addres } from './API'; const api = axios.create({ baseURL: STORYS_addres, @@ -29,6 +29,61 @@ api.interceptors.request.use((config) => { return config; }); +// ── Авто-рефреш access-токена при 401 (2026-06-02) ────────────────────── +// Access-токен живёт 24ч. Раньше студия не рефрешила его → после суток работы +// сохранение проектов падало 401 «через раз» (зависело от свежести токена). +// Теперь при 401 один раз пытаемся обновить access по refreshToken через +// /api/v1/auth/refresh и повторяем исходный запрос. single-flight: при пачке +// параллельных 401 рефреш идёт ОДИН раз, остальные ждут его. +let _refreshPromise = null; +async function _refreshAccessToken() { + const refreshToken = localStorage.getItem('RefreshToken'); + if (!refreshToken) return null; + if (_refreshPromise) return _refreshPromise; + _refreshPromise = (async () => { + try { + // Отдельный axios без интерсепторов — чтобы не зациклить на 401. + const r = await axios.post(`${USER_addres}/api/v1/users/auth/refresh`, + { refreshToken }, { timeout: 15000 }); + const newToken = r.data && r.data.token; + const newRefresh = r.data && r.data.refreshToken; + if (newToken) { + localStorage.setItem('Authorization', newToken); + localStorage.setItem('player_jwt', newToken); + if (newRefresh) localStorage.setItem('RefreshToken', newRefresh); + return newToken; + } + return null; + } catch (e) { + // refresh протух (30 дней) → чистим, юзер перелогинится через минку. + try { localStorage.removeItem('RefreshToken'); } catch (e2) { /* ignore */ } + return null; + } finally { + _refreshPromise = null; + } + })(); + return _refreshPromise; +} + +api.interceptors.response.use( + (resp) => resp, + async (error) => { + const cfg = error && error.config; + const status = error && error.response && error.response.status; + // Только 401 и только один раз на запрос (флаг _retried). + if (status === 401 && cfg && !cfg._retried) { + cfg._retried = true; + const newToken = await _refreshAccessToken(); + if (newToken) { + cfg.headers = cfg.headers || {}; + cfg.headers.Authorization = newToken; + return api(cfg); // повторяем исходный запрос со свежим токеном + } + } + return Promise.reject(error); + }, +); + // ============ ПРОЕКТЫ ============ // Save-операции с увеличенным таймаутом (120с) — для больших карт. diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx index 4794ef1..8bfc26a 100644 --- a/src/auth/AuthContext.jsx +++ b/src/auth/AuthContext.jsx @@ -119,6 +119,10 @@ export function AuthProvider({ children }) { try { localStorage.setItem('Authorization', jwt); localStorage.setItem('player_jwt', jwt); + // 2026-06-02: redeem теперь отдаёт refreshToken — сохраняем, + // чтобы axios-интерсептор мог обновить истёкший access (24ч) + // и сохранение проектов не падало 401 после суток работы. + if (data.refreshToken) localStorage.setItem('RefreshToken', data.refreshToken); } catch {} startWithJwt(jwt); } else { diff --git a/src/editor/Hotbar.jsx b/src/editor/Hotbar.jsx index 3e1cfa2..1b313de 100644 --- a/src/editor/Hotbar.jsx +++ b/src/editor/Hotbar.jsx @@ -16,6 +16,13 @@ import Icon from './Icon'; function Hotbar({ visible, slots = [], activeIndex = 0, onSelect, mobileMode = false }) { if (!visible) return null; + // ГЛОБАЛЬНОЕ ПРАВИЛО (для всех игр тулбокса): если в инвентаре нет ни + // одного предмета — панель инвентаря НЕ показываем вовсе. Пустой hotbar + // из 5 серых ячеек загромождает экран в играх, где инвентарь не нужен. + // Панель появится автоматически, как только в слот попадёт предмет. + const hasAnyItem = Array.isArray(slots) && slots.some((s) => s != null); + if (!hasAnyItem) return null; + const SLOT_COUNT = 5; const cells = []; for (let i = 0; i < SLOT_COUNT; i++) {