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