fix(auth): ����-������ ������ ������ ��� 401 (����� ���������� ����� ���) #22

Open
min wants to merge 3 commits from fix/studio-token-refresh into main
4 changed files with 72 additions and 2 deletions

View File

@ -56,8 +56,12 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Install trufflehog - name: Install trufflehog
run: | 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 \ 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 - name: Run trufflehog
run: | run: |
trufflehog git "file://$(pwd)" \ trufflehog git "file://$(pwd)" \

View File

@ -3,7 +3,7 @@
* Бэкенд: storys-микросервис, префикс /kubikon3d/... * Бэкенд: storys-микросервис, префикс /kubikon3d/...
*/ */
import axios from 'axios'; import axios from 'axios';
import { STORYS_addres } from './API'; import { STORYS_addres, USER_addres } from './API';
const api = axios.create({ const api = axios.create({
baseURL: STORYS_addres, baseURL: STORYS_addres,
@ -29,6 +29,61 @@ api.interceptors.request.use((config) => {
return 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с) — для больших карт. // Save-операции с увеличенным таймаутом (120с) — для больших карт.

View File

@ -119,6 +119,10 @@ export function AuthProvider({ children }) {
try { try {
localStorage.setItem('Authorization', jwt); localStorage.setItem('Authorization', jwt);
localStorage.setItem('player_jwt', jwt); localStorage.setItem('player_jwt', jwt);
// 2026-06-02: redeem теперь отдаёт refreshToken сохраняем,
// чтобы axios-интерсептор мог обновить истёкший access (24ч)
// и сохранение проектов не падало 401 после суток работы.
if (data.refreshToken) localStorage.setItem('RefreshToken', data.refreshToken);
} catch {} } catch {}
startWithJwt(jwt); startWithJwt(jwt);
} else { } else {

View File

@ -16,6 +16,13 @@ import Icon from './Icon';
function Hotbar({ visible, slots = [], activeIndex = 0, onSelect, mobileMode = false }) { function Hotbar({ visible, slots = [], activeIndex = 0, onSelect, mobileMode = false }) {
if (!visible) return null; 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 SLOT_COUNT = 5;
const cells = []; const cells = [];
for (let i = 0; i < SLOT_COUNT; i++) { for (let i = 0; i < SLOT_COUNT; i++) {