Promote shared BIM links through launcher
This commit is contained in:
parent
bf34f35d6c
commit
33d2f03624
|
|
@ -184,6 +184,7 @@ const AUTH_SESSION_API = "/api/auth/session";
|
||||||
const SHARES_API = "/api/shares";
|
const SHARES_API = "/api/shares";
|
||||||
const COMMENTS_API = "/api/comments";
|
const COMMENTS_API = "/api/comments";
|
||||||
const UPLOAD_VERSIONS_API = "/api/uploads/versions";
|
const UPLOAD_VERSIONS_API = "/api/uploads/versions";
|
||||||
|
const SHARE_PROMOTION_TIMEOUT_MS = 4500;
|
||||||
|
|
||||||
const acceptByType = {
|
const acceptByType = {
|
||||||
xkt: ".xkt",
|
xkt: ".xkt",
|
||||||
|
|
@ -620,6 +621,60 @@ const getShareTokenFromPath = () => {
|
||||||
return match ? decodeURIComponent(match[1]) : null;
|
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 = `
|
const COMMENT_TARGET_ICON = `
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M6.5 4.75h11A3.25 3.25 0 0 1 20.75 8v6.25a3.25 3.25 0 0 1-3.25 3.25h-5.26l-5.29 3.82V17.5H6.5a3.25 3.25 0 0 1-3.25-3.25V8A3.25 3.25 0 0 1 6.5 4.75Z"></path>
|
<path d="M6.5 4.75h11A3.25 3.25 0 0 1 20.75 8v6.25a3.25 3.25 0 0 1-3.25 3.25h-5.26l-5.29 3.82V17.5H6.5a3.25 3.25 0 0 1-3.25-3.25V8A3.25 3.25 0 0 1 6.5 4.75Z"></path>
|
||||||
|
|
@ -1794,6 +1849,9 @@ const initializeRuntimeMode = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
applyRuntimeMode();
|
applyRuntimeMode();
|
||||||
|
if (shareToken && viewerMode === "guest") {
|
||||||
|
scheduleShareSessionPromotion(shareToken);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getShareableModelPayload = () => {
|
const getShareableModelPayload = () => {
|
||||||
|
|
@ -6525,7 +6583,8 @@ const loadProjectSnapshot = async (project) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
homeButton.addEventListener("click", () => {
|
homeButton.addEventListener("click", () => {
|
||||||
window.location.assign(getLauncherUrl());
|
const sharePromoteUrl = getShareTokenFromPath() && isGuestMode() ? getShareTopLevelPromotionUrl() : "";
|
||||||
|
window.location.assign(sharePromoteUrl || getLauncherUrl());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (saveProjectButton) {
|
if (saveProjectButton) {
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@
|
||||||
<div class="inspector-select-host" id="displayModeSelect"></div>
|
<div class="inspector-select-host" id="displayModeSelect"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inspector-control-row inspector-control-row--half hidden" id="customDisplayColorRow">
|
<div class="inspector-control-row inspector-control-row--half hidden" id="customDisplayColorRow">
|
||||||
<div class="inspector-control-label">Цвет кастома</div>
|
<div class="inspector-control-label">Цвет</div>
|
||||||
<div class="inspector-color-host" id="customDisplayColorControl"></div>
|
<div class="inspector-color-host" id="customDisplayColorControl"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inspector-control-row inspector-control-row--half" id="lightingModeControl">
|
<div class="inspector-control-row inspector-control-row--half" id="lightingModeControl">
|
||||||
|
|
|
||||||
|
|
@ -805,15 +805,33 @@ const getRequestBaseUrl = (req) => {
|
||||||
return `${proto}://${host}`.replace(/\/$/, "");
|
return `${proto}://${host}`.replace(/\/$/, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildLauncherLoginUrl = (req, nextPath = "/") => {
|
const buildLauncherLaunchUrl = (nextPath = "/") => {
|
||||||
const launchPath = `/api/services/${encodeURIComponent(BIM_SERVICE_SLUG)}/launch`;
|
const launchPath = `/api/services/${encodeURIComponent(BIM_SERVICE_SLUG)}/launch`;
|
||||||
const launchUrl = new URL(launchPath, LAUNCHER_BASE_URL);
|
const launchUrl = new URL(launchPath, LAUNCHER_BASE_URL);
|
||||||
launchUrl.searchParams.set("returnTo", sanitizeReturnTo(nextPath));
|
launchUrl.searchParams.set("returnTo", sanitizeReturnTo(nextPath));
|
||||||
|
return launchUrl.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLauncherLoginUrl = (req, nextPath = "/") => {
|
||||||
const loginUrl = new URL("/auth/login", LAUNCHER_BASE_URL);
|
const loginUrl = new URL("/auth/login", LAUNCHER_BASE_URL);
|
||||||
|
const launchUrl = new URL(buildLauncherLaunchUrl(nextPath));
|
||||||
loginUrl.searchParams.set("returnTo", `${launchUrl.pathname}${launchUrl.search}`);
|
loginUrl.searchParams.set("returnTo", `${launchUrl.pathname}${launchUrl.search}`);
|
||||||
return loginUrl.toString();
|
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 sendAuthRequired = (req, res, url) => {
|
||||||
const nextPath = sanitizeReturnTo(`${url.pathname}${url.search || ""}`);
|
const nextPath = sanitizeReturnTo(`${url.pathname}${url.search || ""}`);
|
||||||
if (!INTERNAL_ACCESS_TOKEN) {
|
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(`<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>BIM share auth</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
const nextPath = ${nextPathJson};
|
||||||
|
const payload = {
|
||||||
|
type: "nodedc:bim-share-promoted",
|
||||||
|
nextPath,
|
||||||
|
createdAt: Date.now()
|
||||||
|
};
|
||||||
|
if (window.parent && window.parent !== window) {
|
||||||
|
window.parent.postMessage(payload, window.location.origin);
|
||||||
|
} else {
|
||||||
|
window.location.replace(nextPath);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`);
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogout = (req, res) => {
|
const handleLogout = (req, res) => {
|
||||||
const sessionId = parseCookies(req.headers.cookie || "")[BIM_SESSION_COOKIE];
|
const sessionId = parseCookies(req.headers.cookie || "")[BIM_SESSION_COOKIE];
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
|
|
@ -1713,6 +1762,9 @@ const handleGetShare = async (req, res, token) => {
|
||||||
}
|
}
|
||||||
const session = getCurrentBimSession(req);
|
const session = getCurrentBimSession(req);
|
||||||
const authenticated = !!session;
|
const authenticated = !!session;
|
||||||
|
const promotionUrls = BIM_AUTH_REQUIRED && !authenticated
|
||||||
|
? buildSharePromotionUrls(safeToken)
|
||||||
|
: {promoteUrl: null, silentPromoteUrl: null};
|
||||||
const versionHistory = await getAssetVersionHistory(req, {
|
const versionHistory = await getAssetVersionHistory(req, {
|
||||||
projectId: share.projectId,
|
projectId: share.projectId,
|
||||||
assetId: share.assetId
|
assetId: share.assetId
|
||||||
|
|
@ -1723,6 +1775,8 @@ const handleGetShare = async (req, res, token) => {
|
||||||
readonly: !authenticated,
|
readonly: !authenticated,
|
||||||
authenticated,
|
authenticated,
|
||||||
canShare: !BIM_AUTH_REQUIRED || authenticated,
|
canShare: !BIM_AUTH_REQUIRED || authenticated,
|
||||||
|
promoteUrl: promotionUrls.promoteUrl,
|
||||||
|
silentPromoteUrl: promotionUrls.silentPromoteUrl,
|
||||||
user: session?.user || null,
|
user: session?.user || null,
|
||||||
viewer: {
|
viewer: {
|
||||||
src: publicUrlForUploadSrc(req, share.src),
|
src: publicUrlForUploadSrc(req, share.src),
|
||||||
|
|
@ -3201,6 +3255,10 @@ const requestHandler = async (req, res) => {
|
||||||
return handleLauncherHandoff(req, res, url);
|
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") {
|
if (req.method === "GET" && url.pathname === "/auth/logout") {
|
||||||
return handleLogout(req, res);
|
return handleLogout(req, res);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue