fix(auth): ����-������ ������ ������ ��� 401 (����� ���������� ����� ���) #22
@ -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с) — для больших карт.
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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++) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user