From 33d2f03624cb167ac0cb262e26958891304651ce Mon Sep 17 00:00:00 2001 From: CODEX Date: Tue, 23 Jun 2026 11:54:31 +0300 Subject: [PATCH] Promote shared BIM links through launcher --- frontend/dcViewer.js | 61 +++++++++++++++++++++++++++++++++++++++++++- frontend/index.html | 2 +- server/index.js | 60 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index 8d6a1a6..172c51d 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -184,6 +184,7 @@ const AUTH_SESSION_API = "/api/auth/session"; const SHARES_API = "/api/shares"; const COMMENTS_API = "/api/comments"; const UPLOAD_VERSIONS_API = "/api/uploads/versions"; +const SHARE_PROMOTION_TIMEOUT_MS = 4500; const acceptByType = { xkt: ".xkt", @@ -620,6 +621,60 @@ const getShareTokenFromPath = () => { return match ? decodeURIComponent(match[1]) : null; }; +const getCurrentReturnPath = () => `${window.location.pathname}${window.location.search || ""}${window.location.hash || ""}`; + +const getShareTopLevelPromotionUrl = () => { + const promoteUrl = startupSharePayload?.promoteUrl; + return typeof promoteUrl === "string" && promoteUrl.trim() ? promoteUrl : ""; +}; + +const scheduleShareSessionPromotion = (shareToken) => { + const silentPromoteUrl = startupSharePayload?.silentPromoteUrl; + if (!shareToken || viewerMode !== "guest" || runtimeSessionInfo.authenticated || typeof silentPromoteUrl !== "string" || !silentPromoteUrl.trim()) { + return; + } + let storageKey = ""; + try { + storageKey = `bimdc-share-promote:${shareToken}`; + if (sessionStorage.getItem(storageKey)) { + return; + } + sessionStorage.setItem(storageKey, String(Date.now())); + } catch (_) { + storageKey = ""; + } + + const iframe = document.createElement("iframe"); + iframe.title = "BIM share auth promotion"; + iframe.tabIndex = -1; + iframe.setAttribute("aria-hidden", "true"); + iframe.style.cssText = "position:fixed;width:1px;height:1px;left:-10000px;top:-10000px;border:0;opacity:0;pointer-events:none;"; + + let done = false; + let timeoutId = null; + const cleanup = () => { + window.removeEventListener("message", onMessage); + if (timeoutId) window.clearTimeout(timeoutId); + iframe.remove(); + }; + const onMessage = (event) => { + if (event.origin !== window.location.origin) return; + const payload = event.data || {}; + if (payload.type !== "nodedc:bim-share-promoted") return; + done = true; + cleanup(); + window.location.replace(payload.nextPath || getCurrentReturnPath()); + }; + + window.addEventListener("message", onMessage); + timeoutId = window.setTimeout(() => { + if (done) return; + cleanup(); + }, SHARE_PROMOTION_TIMEOUT_MS); + iframe.src = silentPromoteUrl; + document.body.appendChild(iframe); +}; + const COMMENT_TARGET_ICON = `
diff --git a/server/index.js b/server/index.js index ccf2dd5..fed3d88 100644 --- a/server/index.js +++ b/server/index.js @@ -805,15 +805,33 @@ const getRequestBaseUrl = (req) => { return `${proto}://${host}`.replace(/\/$/, ""); }; -const buildLauncherLoginUrl = (req, nextPath = "/") => { +const buildLauncherLaunchUrl = (nextPath = "/") => { const launchPath = `/api/services/${encodeURIComponent(BIM_SERVICE_SLUG)}/launch`; const launchUrl = new URL(launchPath, LAUNCHER_BASE_URL); launchUrl.searchParams.set("returnTo", sanitizeReturnTo(nextPath)); + return launchUrl.toString(); +}; + +const buildLauncherLoginUrl = (req, nextPath = "/") => { const loginUrl = new URL("/auth/login", LAUNCHER_BASE_URL); + const launchUrl = new URL(buildLauncherLaunchUrl(nextPath)); loginUrl.searchParams.set("returnTo", `${launchUrl.pathname}${launchUrl.search}`); return loginUrl.toString(); }; +const buildSharePromotionUrls = (token) => { + const safeToken = safeShareToken(token); + if (!safeToken) { + return {promoteUrl: null, silentPromoteUrl: null}; + } + const sharePath = `/share/${encodeURIComponent(safeToken)}`; + const bridgePath = `/auth/share-promoted?next=${encodeURIComponent(sharePath)}`; + return { + promoteUrl: buildLauncherLaunchUrl(sharePath), + silentPromoteUrl: buildLauncherLaunchUrl(bridgePath) + }; +}; + const sendAuthRequired = (req, res, url) => { const nextPath = sanitizeReturnTo(`${url.pathname}${url.search || ""}`); if (!INTERNAL_ACCESS_TOKEN) { @@ -1596,6 +1614,37 @@ const handleLauncherHandoff = async (req, res, url) => { } }; +const handleSharePromoted = (res, url) => { + const nextPath = sanitizeReturnTo(url.searchParams.get("next") || "/"); + const nextPathJson = JSON.stringify(nextPath); + res.statusCode = 200; + res.setHeader("Content-Type", "text/html; charset=utf-8"); + res.setHeader("Cache-Control", "no-store"); + res.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'unsafe-inline'"); + res.end(` + + + + BIM share auth + + + + +`); +}; + const handleLogout = (req, res) => { const sessionId = parseCookies(req.headers.cookie || "")[BIM_SESSION_COOKIE]; if (sessionId) { @@ -1713,6 +1762,9 @@ const handleGetShare = async (req, res, token) => { } const session = getCurrentBimSession(req); const authenticated = !!session; + const promotionUrls = BIM_AUTH_REQUIRED && !authenticated + ? buildSharePromotionUrls(safeToken) + : {promoteUrl: null, silentPromoteUrl: null}; const versionHistory = await getAssetVersionHistory(req, { projectId: share.projectId, assetId: share.assetId @@ -1723,6 +1775,8 @@ const handleGetShare = async (req, res, token) => { readonly: !authenticated, authenticated, canShare: !BIM_AUTH_REQUIRED || authenticated, + promoteUrl: promotionUrls.promoteUrl, + silentPromoteUrl: promotionUrls.silentPromoteUrl, user: session?.user || null, viewer: { src: publicUrlForUploadSrc(req, share.src), @@ -3201,6 +3255,10 @@ const requestHandler = async (req, res) => { return handleLauncherHandoff(req, res, url); } + if (req.method === "GET" && url.pathname === "/auth/share-promoted") { + return handleSharePromoted(res, url); + } + if (req.method === "GET" && url.pathname === "/auth/logout") { return handleLogout(req, res); }