diff --git a/server/control-plane-store.mjs b/server/control-plane-store.mjs index f599dc5..34bef53 100644 --- a/server/control-plane-store.mjs +++ b/server/control-plane-store.mjs @@ -74,6 +74,7 @@ const bimViewerService = { url: "https://bim.nodedc.tech", launchUrl: "https://bim.nodedc.tech", logoutUrl: "https://bim.nodedc.tech/logout", + loginSyncUrl: "https://bim.nodedc.tech/auth/login-sync", accentColor: "#8FE3C8", fallbackGradient: "linear-gradient(132deg, rgba(143, 227, 200, 0.78), rgba(24, 70, 64, 0.9) 46%, #080B0F 84%)", status: "active", @@ -2500,6 +2501,7 @@ function normalizeService(service) { ? "https://bim.nodedc.tech" : service.launchUrl, logoutUrl: service.logoutUrl || "https://bim.nodedc.tech/logout", + loginSyncUrl: service.loginSyncUrl || "https://bim.nodedc.tech/auth/login-sync", authentikApplicationSlug: service.authentikApplicationSlug || "bim-viewer", authentikGroupName: service.authentikGroupName || "nodedc:bim:access", }; @@ -3663,6 +3665,7 @@ function sanitizeServicePatch(payload, service) { "url", "launchUrl", "logoutUrl", + "loginSyncUrl", "iconUrl", "coverImageUrl", "coverMediaKind", diff --git a/server/dev-server.mjs b/server/dev-server.mjs index d6c621c..10116af 100644 --- a/server/dev-server.mjs +++ b/server/dev-server.mjs @@ -214,7 +214,7 @@ app.get("/auth/callback", asyncRoute(async (req, res) => { expiresAt: tokenSet.expires_in ? Date.now() + Number(tokenSet.expires_in) * 1000 : null, }); setNoStore(res); - res.type("html").send(renderLoginCompletePage(pendingLogin.returnTo)); + res.type("html").send(renderLoginCompletePage(pendingLogin.returnTo, getFrontchannelLoginSyncUrls())); })); app.get("/auth/logged-out", (req, res) => { @@ -3924,6 +3924,20 @@ function getFrontchannelLogoutUrls() { return [...new Set(urls.map(normalizeLogoutUrl).filter(Boolean))]; } +function getFrontchannelLoginSyncUrls() { + const launcherData = readLauncherData(); + const services = Array.isArray(launcherData?.services) ? launcherData.services : []; + const urls = []; + + for (const service of services) { + if (typeof service.loginSyncUrl === "string" && service.loginSyncUrl.trim()) { + urls.push(service.loginSyncUrl.trim()); + } + } + + return [...new Set(urls.map(normalizeLogoutUrl).filter(Boolean))]; +} + async function notifyTaskSessionLogout(session) { if (!session?.user || !config.internalAccessToken || !config.taskInternalLogoutUrl) { return; @@ -4016,8 +4030,9 @@ function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) { `; } -function renderLoginCompletePage(returnTo) { +function renderLoginCompletePage(returnTo, frontchannelLoginSyncUrls = []) { const redirectUrlJson = JSON.stringify(sanitizeReturnTo(returnTo)); + const loginSyncUrlsJson = JSON.stringify(frontchannelLoginSyncUrls); return ` @@ -4045,7 +4060,14 @@ function renderLoginCompletePage(returnTo) { try { localStorage.setItem("nodedc:platform-session-event", JSON.stringify(eventPayload)); } catch {} - window.setTimeout(() => window.location.replace(${redirectUrlJson}), 30); + const loginSyncUrls = ${loginSyncUrlsJson}; + for (const loginSyncUrl of loginSyncUrls) { + fetch(loginSyncUrl, { mode: "no-cors", credentials: "include", keepalive: true }).catch(() => undefined); + const image = new Image(); + image.referrerPolicy = "no-referrer"; + image.src = loginSyncUrl; + } + window.setTimeout(() => window.location.replace(${redirectUrlJson}), 180); `; @@ -4134,6 +4156,13 @@ function getSessionSyncAllowedOrigins() { void 0; } } + for (const loginSyncUrl of getFrontchannelLoginSyncUrls()) { + try { + origins.add(new URL(loginSyncUrl).origin); + } catch { + void 0; + } + } return [...origins]; } diff --git a/src/entities/service/types.ts b/src/entities/service/types.ts index b40e2ac..7b0155e 100644 --- a/src/entities/service/types.ts +++ b/src/entities/service/types.ts @@ -14,6 +14,7 @@ export interface Service { url: string; launchUrl?: string | null; logoutUrl?: string | null; + loginSyncUrl?: string | null; iconUrl?: string | null; coverImageUrl?: string | null; coverMediaKind?: MediaKind | null;