diff --git a/frontend/dcViewer.css b/frontend/dcViewer.css index 4e9f48c..3b7b477 100644 --- a/frontend/dcViewer.css +++ b/frontend/dcViewer.css @@ -3220,20 +3220,28 @@ header { } body[data-viewer-mode="guest"] .tb-btn:not([data-action="measure"]):not([data-action="section"]):not([data-action="projection"]), -html[data-share-startup="guest"] body:not([data-viewer-mode="standard"]) .tb-btn:not([data-action="measure"]):not([data-action="section"]):not([data-action="projection"]), +html[data-share-startup] body:not([data-viewer-mode="standard"]) .tb-btn:not([data-action="measure"]):not([data-action="section"]):not([data-action="projection"]), body[data-viewer-mode="guest"] #settingsPanel, -html[data-share-startup="guest"] body:not([data-viewer-mode="standard"]) #settingsPanel, +html[data-share-startup] body:not([data-viewer-mode="standard"]) #settingsPanel, body[data-viewer-mode="guest"] #templatesPanel, -html[data-share-startup="guest"] body:not([data-viewer-mode="standard"]) #templatesPanel, +html[data-share-startup] body:not([data-viewer-mode="standard"]) #templatesPanel, body[data-viewer-mode="guest"] #projectNameSection, -html[data-share-startup="guest"] body:not([data-viewer-mode="standard"]) #projectNameSection, +html[data-share-startup] body:not([data-viewer-mode="standard"]) #projectNameSection, body[data-viewer-mode="guest"] #saveProjectSection, -html[data-share-startup="guest"] body:not([data-viewer-mode="standard"]) #saveProjectSection, +html[data-share-startup] body:not([data-viewer-mode="standard"]) #saveProjectSection, body[data-viewer-mode="guest"] #modelVersionUploadButton, -html[data-share-startup="guest"] body:not([data-viewer-mode="standard"]) #modelVersionUploadButton { +html[data-share-startup] body:not([data-viewer-mode="standard"]) #modelVersionUploadButton { display: none !important; } +html[data-share-startup="pending"] #dropZone { + display: none !important; +} + +html[data-share-startup="pending"] #loader.hidden { + display: block; +} + .version-history-backdrop { position: fixed; inset: 0; diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index 6f0cdbb..410eb7d 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -186,6 +186,7 @@ const COMMENTS_API = "/api/comments"; const UPLOAD_VERSIONS_API = "/api/uploads/versions"; const SHARE_AUTH_GUEST_PARAM = "ndc_bim_guest"; const SHARE_PROMOTION_TIMEOUT_MS = 4500; +const NODEDC_SESSION_EVENT_TYPE = "nodedc:session:logout"; const acceptByType = { xkt: ".xkt", @@ -648,6 +649,101 @@ const clearShareGuestAuthMarker = () => { } }; +const clearShareStartupPending = () => { + if (document.documentElement?.dataset?.shareStartup === "pending") { + delete document.documentElement.dataset.shareStartup; + } +}; + +const isNodeDCSessionLogoutEvent = (value) => ( + value && + typeof value === "object" && + value.type === NODEDC_SESSION_EVENT_TYPE && + typeof value.id === "string" +); + +let nodeDCSessionSyncCleanup = null; +let nodeDCLogoutHandling = false; + +const handleNodeDCSessionLogout = async () => { + if (nodeDCLogoutHandling) return; + nodeDCLogoutHandling = true; + + const logoutRequest = fetch("/logout", { + method: "GET", + credentials: "include", + keepalive: true, + cache: "no-store", + }) + .catch(() => undefined); + + runtimeSessionInfo = { + ...runtimeSessionInfo, + authenticated: false, + canShare: false, + user: null, + }; + + if (startupSharePayload) { + startupSharePayload = { + ...startupSharePayload, + mode: "guest", + readonly: true, + authenticated: false, + canShare: false, + user: null, + }; + } + + if (getShareTokenFromPath()) { + viewerMode = "guest"; + inspectorCollapsed = true; + applyRuntimeMode(); + updatePanelsVisibility(); + nodeDCLogoutHandling = false; + return; + } + + await Promise.race([ + logoutRequest, + new Promise((resolve) => window.setTimeout(resolve, 500)), + ]); + + const loginUrl = new URL("/auth/login", getLauncherUrl()); + window.location.replace(loginUrl.toString()); +}; + +const setupNodeDCSessionSync = () => { + if (nodeDCSessionSyncCleanup || typeof document === "undefined") return; + + let launcherOrigin = ""; + try { + launcherOrigin = new URL(getLauncherUrl()).origin; + } catch (_) { + return; + } + + const iframe = document.createElement("iframe"); + iframe.src = new URL("/auth/session-sync", launcherOrigin).toString(); + iframe.title = "NODE.DC session sync"; + iframe.tabIndex = -1; + iframe.setAttribute("aria-hidden", "true"); + iframe.style.cssText = "position:fixed;width:0;height:0;opacity:0;pointer-events:none;border:0;"; + document.body.appendChild(iframe); + + const handleMessage = (event) => { + if (event.origin !== launcherOrigin || !isNodeDCSessionLogoutEvent(event.data)) return; + void handleNodeDCSessionLogout(); + }; + + window.addEventListener("message", handleMessage); + nodeDCSessionSyncCleanup = () => { + window.removeEventListener("message", handleMessage); + iframe.remove(); + nodeDCSessionSyncCleanup = null; + }; +}; + const scheduleShareSessionPromotion = (shareToken) => { const silentPromoteUrl = startupSharePayload?.silentPromoteUrl; if ( @@ -5212,6 +5308,7 @@ const applyViewerState = (state) => { const initViewer = async () => { await initializeRuntimeMode(); + setupNodeDCSessionSync(); const preloadHidden = document.getElementById("preload-hidden"); if (preloadHidden) { @@ -5842,6 +5939,8 @@ const initViewer = async () => { }); if (!src) { reportViewerEvent("startup-no-src", { search: window.location.search }, "error"); + clearShareStartupPending(); + setLoading(false); return; } @@ -5850,12 +5949,15 @@ const initViewer = async () => { if (!type) { reportViewerEvent("startup-type-error", { src, name, typeParam: params.get("type") }, "error"); showError("Не удалось определить формат модели из URL"); + clearShareStartupPending(); + setLoading(false); return; } dropZone?.classList.add("hidden"); setStatus(`Подготовка: ${name}...`); setLoading(true); + clearShareStartupPending(); let settingsSrc = sharedViewer?.settingsSrc || params.get("settingsSrc") || params.get("sourceSrc") || params.get("originalSrc") || src; const applyQueryViewerOverrides = () => { diff --git a/frontend/index.html b/frontend/index.html index 5bc6142..73d95cb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -16,7 +16,7 @@ diff --git a/server/index.js b/server/index.js index 813a044..08c9e4b 100644 --- a/server/index.js +++ b/server/index.js @@ -1685,15 +1685,22 @@ const handleSharePromoted = (res, url) => { `); }; -const handleLogout = (req, res) => { +const handleLogout = (req, res, {redirect = true} = {}) => { const sessionId = parseCookies(req.headers.cookie || "")[BIM_SESSION_COOKIE]; if (sessionId) { bimSessions.delete(sessionId); } clearBimSessionCookie(res); - res.statusCode = 302; - res.setHeader("Location", "/"); - res.end(); + res.setHeader("Cache-Control", "no-store"); + if (redirect) { + res.statusCode = 302; + res.setHeader("Location", "/"); + res.end(); + return; + } + res.statusCode = 200; + res.setHeader("Content-Type", "text/html; charset=utf-8"); + res.end("NODE.DC BIM session closed."); }; const handleCreateShare = async (req, res) => { @@ -3304,6 +3311,10 @@ const requestHandler = async (req, res) => { return handleLogout(req, res); } + if (req.method === "GET" && url.pathname === "/logout") { + return handleLogout(req, res, {redirect: false}); + } + if (req.method === "GET" && /^\/api\/shares\/[^/]+\/?$/.test(url.pathname)) { return handleGetShare(req, res, shareTokenFromPath(url.pathname)); }