All checks were successful
- no-dupe-keys: дубль ключа эмодзи '🟧' в Icon.jsx
- no-useless-escape: лишний \- в regex (ticketExchange, EmoteGlbParser)
- no-extra-semi: висячие ; в PreviewSkin-route (auto-fix)
Лок. eslint: 0 errors, 118 warnings (< max-warnings 200).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
4.8 KiB
JavaScript
105 lines
4.8 KiB
JavaScript
// ticketExchange — обмен одноразового ticket на постоянный JWT.
|
||
//
|
||
// Поток:
|
||
// rublox.pro / Майнкрафтия → POST /api-user/api/v1/auth/play-ticket → { ticket }
|
||
// браузер открывает player.rublox.pro/<id>#ticket=<ticket>
|
||
// плеер → POST /api-user/api/v1/auth/redeem-ticket { ticket } → { jwt, user }
|
||
// плеер сохраняет JWT в localStorage["player_jwt"]
|
||
//
|
||
// На стороне нашего плеера здесь ТОЛЬКО redeem-сторона.
|
||
// Сторону play-ticket делают rublox.pro и Майнкрафтия (Этапы 5 и 6).
|
||
|
||
import axios from 'axios';
|
||
import { USER_addres } from '../api/API';
|
||
|
||
const JWT_KEY = 'player_jwt';
|
||
|
||
/**
|
||
* Обменять ticket на JWT + user-данные.
|
||
* Возвращает { jwt, user } или кидает ошибку (которую вызывающий ловит
|
||
* и показывает в LoadingScreen).
|
||
*/
|
||
export async function redeemTicket(ticket) {
|
||
const r = await axios.post(
|
||
`${USER_addres}/api/v1/auth/redeem-ticket`,
|
||
{ ticket },
|
||
{ timeout: 15000 },
|
||
);
|
||
return r.data;
|
||
}
|
||
|
||
// «Зеркало» в Authorization — это ключ, который читают engine, скрипты
|
||
// уровней (game.save.*), KubikonBugReport, KubikonChatPanel и ещё ~10
|
||
// мест в скопированном 1-в-1 коде. Чтобы НЕ править engine (по правилу
|
||
// «1-в-1, ничего не ломать»), JWT кладётся в ОБА ключа:
|
||
// - player_jwt: новый канон плеера (читают auth/PlayerAuth.jsx,
|
||
// api/Kubikon3DService.js)
|
||
// - Authorization: совместимость с Майнкрафтия-кодом из engine
|
||
// (читают GameRuntime._saveBaseUrl, GdPlayerModeSkin, GdPlayerTrail,
|
||
// GdPortalArch, KubikonChatPanel, KubikonComments, KubikonBugReport,
|
||
// KubikonPlayer.jsx userId-getter и многое другое).
|
||
//
|
||
// Это разово при логине, читается из обоих мест — синхронизация
|
||
// гарантирована.
|
||
const AUTH_KEY_MIRROR = 'Authorization';
|
||
|
||
export function saveJWT(jwt) {
|
||
try {
|
||
localStorage.setItem(JWT_KEY, jwt);
|
||
localStorage.setItem(AUTH_KEY_MIRROR, jwt);
|
||
} catch (e) {}
|
||
}
|
||
|
||
export function getJWT() {
|
||
try { return localStorage.getItem(JWT_KEY); } catch (e) { return null; }
|
||
}
|
||
|
||
export function clearJWT() {
|
||
try {
|
||
localStorage.removeItem(JWT_KEY);
|
||
localStorage.removeItem(AUTH_KEY_MIRROR);
|
||
} catch (e) {}
|
||
}
|
||
|
||
/**
|
||
* Достать ticket из window.location.hash. Возвращает hex-строку или null.
|
||
* Hash чистится отдельно (после успешного redeem) через history.replaceState,
|
||
* чтобы юзер не мог поделиться URL с одноразовым ticket'ом (даже после
|
||
* его сжигания это просто плохой UX).
|
||
*/
|
||
export function readTicketFromHash() {
|
||
if (typeof window === 'undefined') return null;
|
||
const m = /(?:^|[#&])ticket=([0-9a-fA-F]+)/.exec(window.location.hash || '');
|
||
return m ? m[1] : null;
|
||
}
|
||
|
||
/**
|
||
* Достать team_jwt из window.location.hash. Используется когда модератор
|
||
* с team.rublox.pro открывает draft/review-игру в плеере для проверки.
|
||
*
|
||
* Поток: team-фронт берёт свой team_jwt из localStorage и формирует
|
||
* URL вида `https://player.rublox.pro/<id>#team_jwt=<jwt>`. Мы кладём
|
||
* этот jwt прямо в localStorage плеера (без redeem-flow) — team_jwt и
|
||
* user_jwt подписаны одним секретом, user-сервис их обрабатывает
|
||
* одинаково. На стороне storys get_project ищет moderator-роль через
|
||
* team-flask (см. team_mod_auth.py) и пропускает к draft/review-проектам.
|
||
*
|
||
* Безопасность: team_jwt и так уже выдаётся модератору после входа в
|
||
* team.rublox.pro; передача его в hash другому поддомену того же origin
|
||
* (rublox.pro) не повышает риск. Hash не уходит в логи серверов.
|
||
*/
|
||
export function readTeamJwtFromHash() {
|
||
if (typeof window === 'undefined') return null;
|
||
// JWT-формат: header.payload.signature — три blob'а из base64url, точки.
|
||
const m = /(?:^|[#&])team_jwt=([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/
|
||
.exec(window.location.hash || '');
|
||
return m ? m[1] : null;
|
||
}
|
||
|
||
export function clearTicketFromUrl() {
|
||
if (typeof window === 'undefined' || !window.history?.replaceState) return;
|
||
try {
|
||
window.history.replaceState(null, '', window.location.pathname + window.location.search);
|
||
} catch (e) {}
|
||
}
|