Add optional BIM share launch handoff

This commit is contained in:
DCCONSTRUCTIONS 2026-06-23 14:08:12 +03:00
parent 4edca8c7cc
commit 754142b2ca
1 changed files with 117 additions and 47 deletions

View File

@ -405,6 +405,40 @@ app.get("/api/apps", (req, res) => {
res.json({ apps: getAppsForUser(sessionAccess.runtimeContext.groups) });
});
app.get("/api/services/:serviceSlug/optional-launch", (req, res) => {
const serviceSlug = sanitizeServiceSlug(req.params.serviceSlug);
const fallbackPath = req.query.fallbackTo || req.query.returnTo || "/";
const fallbackUrl = buildOptionalServiceFallbackUrl(serviceSlug, null, fallbackPath);
const session = getCurrentSession(req);
if (!session) {
setNoStore(res);
res.redirect(fallbackUrl);
return;
}
const sessionAccess = getSessionAccessState(session);
if (!sessionAccess.ok) {
expireSession(res, session);
setNoStore(res);
res.redirect(fallbackUrl);
return;
}
const runtimeContext = sessionAccess.runtimeContext;
const app = getAppsForUser(runtimeContext.groups).find((candidate) => candidate.slug === serviceSlug);
const appFallbackUrl = buildOptionalServiceFallbackUrl(serviceSlug, app, fallbackPath);
if (!app || !app.hasAccess || app.status !== "active") {
setNoStore(res);
res.redirect(appFallbackUrl);
return;
}
redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/");
});
app.get("/api/services/:serviceSlug/launch", requireSession, (req, res) => {
const serviceSlug = sanitizeServiceSlug(req.params.serviceSlug);
const runtimeContext = getRuntimeSessionContext(req.nodedcSession);
@ -415,53 +449,7 @@ app.get("/api/services/:serviceSlug/launch", requireSession, (req, res) => {
return;
}
if (isEngineServiceSlug(serviceSlug)) {
const handoffToken = createServiceHandoff("nodedc", runtimeContext.user);
const targetUrl = new URL("/auth/nodedc/handoff/", getEngineLaunchTargetUrl(app));
const nextPath = sanitizeReturnTo(req.query.next_path || req.query.returnTo || "/");
targetUrl.searchParams.set("token", handoffToken);
if (nextPath && nextPath !== "/") {
targetUrl.searchParams.set("next_path", nextPath);
}
res.redirect(targetUrl.toString());
return;
}
if (isBimViewerServiceSlug(serviceSlug)) {
const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user);
const targetUrl = new URL("/auth/nodedc/handoff", getBimViewerBaseUrl(app));
const nextPath = sanitizeReturnTo(req.query.next_path || req.query.returnTo || "/");
targetUrl.searchParams.set("token", handoffToken);
if (nextPath && nextPath !== "/") {
targetUrl.searchParams.set("next_path", nextPath);
}
res.redirect(targetUrl.toString());
return;
}
if (serviceSlug !== "task-manager") {
res.redirect(app.openUrl || app.url || "/");
return;
}
const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user);
const taskBaseUrl = getTaskBaseUrl();
const targetUrl = new URL("/auth/nodedc/handoff/", taskBaseUrl);
const nextPath = sanitizeReturnTo(req.query.next_path || req.query.returnTo || "/");
targetUrl.searchParams.set("token", handoffToken);
if (nextPath && nextPath !== "/") {
targetUrl.searchParams.set("next_path", nextPath);
}
res.redirect(targetUrl.toString());
redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/");
});
app.post("/api/internal/handoff/consume", (req, res) => {
@ -3126,6 +3114,88 @@ function getBimViewerBaseUrl(app = null) {
).replace(/\/$/, "");
}
function findServiceCatalogAppBySlug(serviceSlug) {
const slug = sanitizeServiceSlug(serviceSlug);
try {
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC optional service fallback" });
return snapshot.data.services.find((candidate) => sanitizeServiceSlug(candidate.slug) === slug) || null;
} catch {
return null;
}
}
function buildOptionalServiceFallbackUrl(serviceSlug, app, fallbackPath) {
const path = sanitizeReturnTo(fallbackPath || "/");
const serviceApp = app || findServiceCatalogAppBySlug(serviceSlug);
let baseUrl = config.appBaseUrl;
if (isBimViewerServiceSlug(serviceSlug)) {
baseUrl = getBimViewerBaseUrl(serviceApp);
} else if (isEngineServiceSlug(serviceSlug)) {
baseUrl = getEngineLaunchTargetUrl(serviceApp);
} else if (serviceSlug === "task-manager") {
baseUrl = getTaskBaseUrl();
} else if (serviceApp?.openUrl || serviceApp?.url) {
baseUrl = serviceApp.openUrl || serviceApp.url;
}
try {
return new URL(path, String(baseUrl || config.appBaseUrl).replace(/\/$/, "")).toString();
} catch {
return new URL("/", config.appBaseUrl).toString();
}
}
function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, returnTo = "/") {
if (isEngineServiceSlug(serviceSlug)) {
const handoffToken = createServiceHandoff("nodedc", runtimeContext.user);
const targetUrl = new URL("/auth/nodedc/handoff/", getEngineLaunchTargetUrl(app));
const nextPath = sanitizeReturnTo(returnTo);
targetUrl.searchParams.set("token", handoffToken);
if (nextPath && nextPath !== "/") {
targetUrl.searchParams.set("next_path", nextPath);
}
res.redirect(targetUrl.toString());
return;
}
if (isBimViewerServiceSlug(serviceSlug)) {
const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user);
const targetUrl = new URL("/auth/nodedc/handoff", getBimViewerBaseUrl(app));
const nextPath = sanitizeReturnTo(returnTo);
targetUrl.searchParams.set("token", handoffToken);
if (nextPath && nextPath !== "/") {
targetUrl.searchParams.set("next_path", nextPath);
}
res.redirect(targetUrl.toString());
return;
}
if (serviceSlug !== "task-manager") {
res.redirect(app.openUrl || app.url || "/");
return;
}
const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user);
const targetUrl = new URL("/auth/nodedc/handoff/", getTaskBaseUrl());
const nextPath = sanitizeReturnTo(returnTo);
targetUrl.searchParams.set("token", handoffToken);
if (nextPath && nextPath !== "/") {
targetUrl.searchParams.set("next_path", nextPath);
}
res.redirect(targetUrl.toString());
}
function getEnginePublicBaseUrl() {
return String(
process.env.NODEDC_ENGINE_PUBLIC_URL ||