// ticketExchange — обмен одноразового ticket на постоянный JWT. // // Поток: // rublox.pro / Майнкрафтия → POST /api-user/api/v1/auth/play-ticket → { ticket } // браузер открывает player.rublox.pro/#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/#team_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) {} }