player/src/auth/ticketExchange.js
МИН 9c79da4ce5
All checks were successful
CI / Lint (pull_request) Successful in 58s
CI / Build (pull_request) Successful in 1m34s
CI / Secret scan (pull_request) Successful in 2m28s
CI / PR size check (pull_request) Successful in 6s
CI / Deploy to S1 + S2 (pull_request) Has been skipped
fix(lint): устранить 8 eslint-ошибок (предсущ., всплыли после починки конфига)
- 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>
2026-05-30 22:30:50 +03:00

105 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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) {}
}