Compare commits
No commits in common. "7cbcdce6f98f6d96c1c8faef9250677b9973127e" and "70731dae3179cb89699eaab492cc0e4bb7edf282" have entirely different histories.
7cbcdce6f9
...
70731dae31
@ -71,24 +71,6 @@ export function AuthProvider({ children }) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// После успешного входа: если есть запомненная collab-ссылка и мы сейчас
|
|
||||||
// НЕ на ней — переходим. Это и есть «возврат на приглашение» (минкин логин
|
|
||||||
// сам этого не делает). Чистим ключ, чтобы не зациклить.
|
|
||||||
const maybeResumePendingCollab = () => {
|
|
||||||
try {
|
|
||||||
const pending = localStorage.getItem('kbn_pending_collab_url');
|
|
||||||
if (!pending) return;
|
|
||||||
localStorage.removeItem('kbn_pending_collab_url');
|
|
||||||
// уже на нужной collab-странице? не дёргаем
|
|
||||||
if (window.location.href === pending) return;
|
|
||||||
const u = new URL(pending);
|
|
||||||
// безопасность: переходим только в пределах этого же origin (студии)
|
|
||||||
if (u.origin !== window.location.origin) return;
|
|
||||||
if (!/[?&]collab=/.test(pending)) return;
|
|
||||||
window.location.replace(pending);
|
|
||||||
} catch {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const startWithJwt = (raw) => {
|
const startWithJwt = (raw) => {
|
||||||
const payload = decodeJwtSafe(raw);
|
const payload = decodeJwtSafe(raw);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
@ -114,9 +96,6 @@ export function AuthProvider({ children }) {
|
|||||||
role: profile.role || 'user',
|
role: profile.role || 'user',
|
||||||
jwt: raw,
|
jwt: raw,
|
||||||
});
|
});
|
||||||
// Возврат на инвайт-ссылку после входа: минка вернула нас в студию
|
|
||||||
// (на главную/кабинет), но мы запомнили collab-URL — идём на него.
|
|
||||||
maybeResumePendingCollab();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@ -170,42 +149,3 @@ export function AuthProvider({ children }) {
|
|||||||
|
|
||||||
return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Редирект незалогиненного пользователя на форму входа Рублокса (SSO).
|
|
||||||
* В студии НЕТ собственной страницы /login — вход живёт на КОРНЕ домена
|
|
||||||
* Рублокса (`https://rublox.pro/login`, НЕ `/app/login` — тот даёт 404).
|
|
||||||
* После входа лендинг возвращает в студию/плеер с #ticket=<hex>
|
|
||||||
* (его обменивает AuthProvider выше). `return`-параметр сохраняет ПОЛНЫЙ
|
|
||||||
* текущий URL (включая ?collab=<token>), чтобы человек попал ровно на ту
|
|
||||||
* инвайт-ссылку, с которой пришёл.
|
|
||||||
*/
|
|
||||||
// Ключ, под которым студия запоминает collab-ссылку на время входа.
|
|
||||||
// Основной путь возврата — rublox.pro/login читает наш ?return и сам
|
|
||||||
// редиректит обратно с #ticket (см. rublox-site LoginPage.finishLogin).
|
|
||||||
// Этот pending-ключ — РЕЗЕРВ на случай, если юзер вернулся в студию иным
|
|
||||||
// путём: AuthProvider после входа перекинет на запомненную ссылку.
|
|
||||||
export const PENDING_COLLAB_KEY = 'kbn_pending_collab_url';
|
|
||||||
|
|
||||||
export function redirectToLogin() {
|
|
||||||
const env = (typeof import.meta !== 'undefined' && import.meta.env) || {};
|
|
||||||
// VITE_RUBLOX_HOME = "https://rublox.pro/app" — нам нужен ORIGIN (без /app),
|
|
||||||
// т.к. форма входа сидит на корне домена: https://rublox.pro/login.
|
|
||||||
const rawHome = env.VITE_RUBLOX_HOME || 'https://rublox.pro/app';
|
|
||||||
let origin;
|
|
||||||
try {
|
|
||||||
origin = new URL(rawHome).origin; // https://rublox.pro
|
|
||||||
} catch {
|
|
||||||
origin = rawHome.replace(/\/app\/?$/, '').replace(/\/$/, '');
|
|
||||||
}
|
|
||||||
// Если уходим со страницы с инвайт-токеном — запомним её, чтобы вернуться
|
|
||||||
// после входа (минка сама вернёт только в кабинет, не на эту ссылку).
|
|
||||||
try {
|
|
||||||
const cur = window.location.href;
|
|
||||||
if (/[?&]collab=/.test(cur)) {
|
|
||||||
localStorage.setItem(PENDING_COLLAB_KEY, cur);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
const back = encodeURIComponent(window.location.href);
|
|
||||||
window.location.href = `${origin}/login?return=${back}`;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { jwtDecode } from 'jwt-decode';
|
import { jwtDecode } from 'jwt-decode';
|
||||||
import { useAuth, redirectToLogin } from '../auth/AuthContext.jsx';
|
import { useAuth } from '../auth/AuthContext.jsx';
|
||||||
import { useSanctions } from '../auth/SanctionsContext.jsx';
|
import { useSanctions } from '../auth/SanctionsContext.jsx';
|
||||||
import { BabylonScene } from './engine/BabylonScene';
|
import { BabylonScene } from './engine/BabylonScene';
|
||||||
import { StudioCollab } from './engine/StudioCollab';
|
import { StudioCollab } from './engine/StudioCollab';
|
||||||
@ -470,9 +470,6 @@ const KubikonEditor = () => {
|
|||||||
// Team Create — клиент совместного редактирования + presence-overlay.
|
// Team Create — клиент совместного редактирования + presence-overlay.
|
||||||
const collabRef = useRef(null);
|
const collabRef = useRef(null);
|
||||||
const collabOverlayRef = useRef(null);
|
const collabOverlayRef = useRef(null);
|
||||||
// ref на initCollab — чтобы вызвать его из doSave (объявлен раньше initCollab)
|
|
||||||
// после создания нового проекта, не дожидаясь перезагрузки страницы.
|
|
||||||
const initCollabRef = useRef(null);
|
|
||||||
const [collabActive, setCollabActive] = useState(false); // подключены к комнате
|
const [collabActive, setCollabActive] = useState(false); // подключены к комнате
|
||||||
const [collabPeers, setCollabPeers] = useState(0); // сколько ДРУГИХ соавторов
|
const [collabPeers, setCollabPeers] = useState(0); // сколько ДРУГИХ соавторов
|
||||||
// Роль в коллабе: 'owner' (владелец) | 'collab' (приглашённый по ссылке).
|
// Роль в коллабе: 'owner' (владелец) | 'collab' (приглашённый по ссылке).
|
||||||
@ -991,12 +988,10 @@ const KubikonEditor = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
// Доступ открыт всем авторизованным (раньше — только admin)
|
// Доступ открыт всем авторизованным (раньше — только admin)
|
||||||
// Нет своей страницы /login — уходим на вход минки (SSO) с возвратом
|
|
||||||
// на текущий URL (важно для инвайт-ссылок ?collab=<token>).
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
redirectToLogin();
|
navigate('/login', { replace: true });
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, isLoading]);
|
}, [isAuthenticated, isLoading, navigate]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Реальное сохранение на сервер.
|
* Реальное сохранение на сервер.
|
||||||
@ -1212,11 +1207,6 @@ const KubikonEditor = () => {
|
|||||||
if (newId) {
|
if (newId) {
|
||||||
currentProjectIdRef.current = newId;
|
currentProjectIdRef.current = newId;
|
||||||
window.history.replaceState({}, '', `/edit/${newId}`);
|
window.history.replaceState({}, '', `/edit/${newId}`);
|
||||||
// Team Create: проект только что получил реальный ID — поднимаем
|
|
||||||
// совместную сессию сразу, без перезагрузки страницы. Иначе
|
|
||||||
// индикатор соавторов и инвайт не появлялись, пока юзер не
|
|
||||||
// поставит блок → сохранит → обновит (баг 2026-06-08).
|
|
||||||
try { initCollabRef.current?.(Number(newId)); } catch (_) {}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await Kubikon3DApi.updateProject(currentProjectIdRef.current, payload);
|
await Kubikon3DApi.updateProject(currentProjectIdRef.current, payload);
|
||||||
@ -1328,8 +1318,6 @@ const KubikonEditor = () => {
|
|||||||
console.warn('[collab] init skipped:', e?.message || e);
|
console.warn('[collab] init skipped:', e?.message || e);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
// Держим актуальную ссылку на initCollab для вызова из doSave (см. выше).
|
|
||||||
initCollabRef.current = initCollab;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Team Create: «Пригласить» — запросить collab-токен у realtime, собрать
|
* Team Create: «Пригласить» — запросить collab-токен у realtime, собрать
|
||||||
@ -1337,25 +1325,10 @@ const KubikonEditor = () => {
|
|||||||
*/
|
*/
|
||||||
const handleInvite = useCallback(async () => {
|
const handleInvite = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
// Реальный ID берём из ref (он обновляется после сохранения нового
|
if (!/^\d+$/.test(id)) { alert('Сначала сохрани проект.'); return; }
|
||||||
// проекта), а НЕ из URL-параметра id — при создании нового проекта
|
|
||||||
// URL остаётся /edit/new даже после успешного сохранения, поэтому
|
|
||||||
// проверка по id давала вечное «Сначала сохрани» (баг 2026-06-08).
|
|
||||||
let pid = currentProjectIdRef.current;
|
|
||||||
// Не сохранён ещё? Попробуем сохранить прямо сейчас, затем пригласить.
|
|
||||||
if (pid == null) {
|
|
||||||
try {
|
|
||||||
await doSave?.();
|
|
||||||
} catch (_) { /* ignore — проверим pid ниже */ }
|
|
||||||
pid = currentProjectIdRef.current;
|
|
||||||
}
|
|
||||||
if (pid == null || !/^\d+$/.test(String(pid))) {
|
|
||||||
alert('Сначала сохрани проект — добавь хотя бы один объект на сцену и нажми «Сохранить».');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tokenRaw = localStorage.getItem('Authorization') || localStorage.getItem('jwt') || '';
|
const tokenRaw = localStorage.getItem('Authorization') || localStorage.getItem('jwt') || '';
|
||||||
const base = (REALTIME_HTTP || '').replace(/\/$/, '');
|
const base = (REALTIME_HTTP || '').replace(/\/$/, '');
|
||||||
const res = await fetch(`${base}/studio-invite/${pid}`, {
|
const res = await fetch(`${base}/studio-invite/${id}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: tokenRaw, 'Content-Type': 'application/json' },
|
headers: { Authorization: tokenRaw, 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
@ -1365,7 +1338,7 @@ const KubikonEditor = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { token } = await res.json();
|
const { token } = await res.json();
|
||||||
const link = `${window.location.origin}/edit/${pid}?collab=${token}`;
|
const link = `${window.location.origin}/edit/${id}?collab=${token}`;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(link);
|
await navigator.clipboard.writeText(link);
|
||||||
collabOverlayRef.current?.toast('Ссылка-приглашение скопирована! Отправь другу.');
|
collabOverlayRef.current?.toast('Ссылка-приглашение скопирована! Отправь другу.');
|
||||||
@ -1376,7 +1349,7 @@ const KubikonEditor = () => {
|
|||||||
console.warn('[collab] invite failed', e);
|
console.warn('[collab] invite failed', e);
|
||||||
alert('Не удалось создать приглашение. Realtime недоступен?');
|
alert('Не удалось создать приглашение. Realtime недоступен?');
|
||||||
}
|
}
|
||||||
}, [doSave]);
|
}, [id]);
|
||||||
|
|
||||||
// Инициализация Babylon + загрузка проекта (если редактируем существующий)
|
// Инициализация Babylon + загрузка проекта (если редактируем существующий)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -1392,10 +1365,6 @@ const KubikonEditor = () => {
|
|||||||
if (!canvasRef.current) return;
|
if (!canvasRef.current) return;
|
||||||
|
|
||||||
const scene = new BabylonScene(canvasRef.current);
|
const scene = new BabylonScene(canvasRef.current);
|
||||||
// Студия-редактор: тестовый «Запуск» НЕ должен показывать стартовый
|
|
||||||
// экран загрузки игры (он нужен только в плеере на rublox.pro, чтобы
|
|
||||||
// дать время подгрузить ассеты). В студии всё уже в памяти.
|
|
||||||
scene._editorMode = true;
|
|
||||||
scene.init();
|
scene.init();
|
||||||
sceneRef.current = scene;
|
sceneRef.current = scene;
|
||||||
|
|
||||||
|
|||||||
@ -5972,10 +5972,6 @@ export class BabylonScene {
|
|||||||
* Зовётся из enterPlayMode; держится минимум `duration` сек либо до готовности сцены.
|
* Зовётся из enterPlayMode; держится минимум `duration` сек либо до готовности сцены.
|
||||||
*/
|
*/
|
||||||
showStartupLoadingScreen() {
|
showStartupLoadingScreen() {
|
||||||
// В студии-редакторе тестовый «Запуск» НИКОГДА не показывает стартовый
|
|
||||||
// экран загрузки — он нужен только в плеере (rublox.pro «Играть»),
|
|
||||||
// чтобы дать время подгрузить ассеты. В редакторе всё уже в памяти.
|
|
||||||
if (this._editorMode) return;
|
|
||||||
const cfg = this._loadingConfig;
|
const cfg = this._loadingConfig;
|
||||||
if (!cfg || cfg.enabled === false) return;
|
if (!cfg || cfg.enabled === false) return;
|
||||||
if (!this.gameRuntime) return;
|
if (!this.gameRuntime) return;
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import ModalOverlay from '../editor/ModalOverlay';
|
|||||||
import SkinShopOverlay from '../editor/SkinShopOverlay';
|
import SkinShopOverlay from '../editor/SkinShopOverlay';
|
||||||
import KubikonBugReportButton from '../components/KubikonBugReport/KubikonBugReportButton';
|
import KubikonBugReportButton from '../components/KubikonBugReport/KubikonBugReportButton';
|
||||||
import KubikonChatPanel from './KubikonChatPanel';
|
import KubikonChatPanel from './KubikonChatPanel';
|
||||||
import { useAuth, redirectToLogin } from '../auth/AuthContext.jsx';
|
import { useAuth } from '../auth/AuthContext.jsx';
|
||||||
import RublocsLogo from '../components/RublocsLogo/RublocsLogo';
|
import RublocsLogo from '../components/RublocsLogo/RublocsLogo';
|
||||||
import useDeviceType from '../hooks/useDeviceType';
|
import useDeviceType from '../hooks/useDeviceType';
|
||||||
import KubikonMobileControls from './KubikonMobileControls';
|
import KubikonMobileControls from './KubikonMobileControls';
|
||||||
@ -96,14 +96,13 @@ const KubikonPlayer = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Только зарегистрированные. Если гость / без токена — на вход минки (SSO)
|
// Только зарегистрированные. Если гость / без токена — на логин.
|
||||||
// с возвратом на текущий URL (своей страницы /login в студии нет).
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authLoading) return;
|
if (authLoading) return;
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
redirectToLogin();
|
navigate('/login', { replace: true });
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, authLoading]);
|
}, [isAuthenticated, authLoading, navigate]);
|
||||||
|
|
||||||
const canvasRef = useRef(null);
|
const canvasRef = useRef(null);
|
||||||
const sceneRef = useRef(null);
|
const sceneRef = useRef(null);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user