From 8fe83df899910791e3a5296a3b338e2722ecc2aa Mon Sep 17 00:00:00 2001 From: min Date: Tue, 2 Jun 2026 14:20:37 +0300 Subject: [PATCH 1/3] =?UTF-8?q?fix(auth):=20=D0=B0=D0=B2=D1=82=D0=BE-?= =?UTF-8?q?=D1=80=D0=B5=D1=84=D1=80=D0=B5=D1=88=20access=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20401=20+=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20refreshToken=20=D0=B8=D0=B7=20redeem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Студия не умела рефрешить токен (access живёт 24ч) → сохранение проектов падало 401 после суток работы («через раз»). Теперь: AuthContext сохраняет refreshToken из redeem-ticket; axios-интерсептор при 401 обновляет access через /auth/refresh (single-flight) и повторяет запрос. + глобальное правило Hotbar: пустой инвентарь не показывает панель. Co-Authored-By: Claude Opus 4.8 --- src/api/Kubikon3DService.js | 57 ++++++++++++++++++++++++++++++++++++- src/auth/AuthContext.jsx | 4 +++ src/editor/Hotbar.jsx | 7 +++++ 3 files changed, 67 insertions(+), 1 deletion(-) 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++) { -- 2.47.2 From 201c54d1799a7fb8877505d075f15a67b37a645a Mon Sep 17 00:00:00 2001 From: min Date: Tue, 2 Jun 2026 14:26:27 +0300 Subject: [PATCH 2/3] =?UTF-8?q?ci:=20re-run=20=E2=80=94=20secret-scan=20?= =?UTF-8?q?=D1=83=D0=BF=D0=B0=D0=BB=20=D0=BD=D0=B0=20install=20trufflehog?= =?UTF-8?q?=20(=D0=B8=D0=BD=D1=84=D1=80=D0=B0,=20=D0=BD=D0=B5=20=D1=81?= =?UTF-8?q?=D0=B5=D0=BA=D1=80=D0=B5=D1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 -- 2.47.2 From 5c5ae76e5c43d98fd0e9e3330330bce47c77d607 Mon Sep 17 00:00:00 2001 From: min Date: Tue, 2 Jun 2026 14:32:25 +0300 Subject: [PATCH 3/3] =?UTF-8?q?ci(secret-scan):=20=D0=B7=D0=B0=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=20trufflehog=20v3.90.5=20(latest=203.9?= =?UTF-8?q?5.4=20=3D=20404=20=D0=BD=D0=B0=20=D0=B1=D0=B8=D0=BD=D0=B0=D1=80?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream trufflehog latest-тег указывал на релиз без выложенного бинарника → install HTTP 404 → secret-scan падал на всех PR. Пин на стабильную версию. Co-Authored-By: Claude Opus 4.8 --- .gitea/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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)" \ -- 2.47.2