From f02865512f87bf4b219f714cafbc846ab4081c57 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Mon, 4 May 2026 23:24:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=A3=D0=9D=D0=9A=D0=A6=D0=98=D0=98=20-?= =?UTF-8?q?=20NODEDC=20TASK:=20listen=20for=20platform=20logout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plane-src/apps/web/app/root.tsx | 2 + .../auth-screens/nodedc-session-sync.tsx | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 plane-src/apps/web/core/components/auth-screens/nodedc-session-sync.tsx diff --git a/plane-src/apps/web/app/root.tsx b/plane-src/apps/web/app/root.tsx index 1dcf8ba..8bff47e 100644 --- a/plane-src/apps/web/app/root.tsx +++ b/plane-src/apps/web/app/root.tsx @@ -24,6 +24,7 @@ import globalStyles from "@/styles/globals.css?url"; import type { Route } from "./+types/root"; import designConfig from "../design.config.json"; // components +import { NodeDCSessionSync } from "@/components/auth-screens/nodedc-session-sync"; // local import { CustomErrorComponent } from "./error"; import { AppProvider } from "./provider"; @@ -181,6 +182,7 @@ export const meta: Route.MetaFunction = () => [ export default function Root() { return ( +
diff --git a/plane-src/apps/web/core/components/auth-screens/nodedc-session-sync.tsx b/plane-src/apps/web/core/components/auth-screens/nodedc-session-sync.tsx new file mode 100644 index 0000000..e40a08a --- /dev/null +++ b/plane-src/apps/web/core/components/auth-screens/nodedc-session-sync.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect } from "react"; + +import { buildNodeDCLauncherUrl, shouldUseNodeDCOIDC } from "@/helpers/nodedc-auth"; + +const NODEDC_SESSION_EVENT_TYPE = "nodedc:session:logout"; + +interface NodeDCSessionEvent { + type: typeof NODEDC_SESSION_EVENT_TYPE; + id: string; +} + +export function NodeDCSessionSync() { + useEffect(() => { + if (!shouldUseNodeDCOIDC()) return; + + const launcherOrigin = new URL(buildNodeDCLauncherUrl()).origin; + const syncUrl = new URL("/auth/session-sync", launcherOrigin).toString(); + const loginUrl = new URL("/auth/login?prompt=login", launcherOrigin).toString(); + let isRedirecting = false; + + const iframe = document.createElement("iframe"); + iframe.src = syncUrl; + iframe.title = "NODE.DC session sync"; + iframe.tabIndex = -1; + iframe.setAttribute("aria-hidden", "true"); + iframe.style.position = "fixed"; + iframe.style.width = "0"; + iframe.style.height = "0"; + iframe.style.opacity = "0"; + iframe.style.pointerEvents = "none"; + iframe.style.border = "0"; + document.body.appendChild(iframe); + + const handleLogout = () => { + if (isRedirecting) return; + + isRedirecting = true; + const redirectToLogin = () => window.location.replace(loginUrl); + const fallbackTimer = window.setTimeout(redirectToLogin, 500); + + fetch("/logout", { method: "GET", credentials: "include", keepalive: true }) + .catch(() => undefined) + .finally(() => { + window.clearTimeout(fallbackTimer); + redirectToLogin(); + }); + }; + + const handleMessage = (event: MessageEvent) => { + if (event.origin !== launcherOrigin || !isNodeDCSessionEvent(event.data)) return; + handleLogout(); + }; + + window.addEventListener("message", handleMessage); + + return () => { + window.removeEventListener("message", handleMessage); + iframe.remove(); + }; + }, []); + + return null; +} + +function isNodeDCSessionEvent(value: unknown): value is NodeDCSessionEvent { + if (!value || typeof value !== "object") return false; + + const event = value as Partial; + return event.type === NODEDC_SESSION_EVENT_TYPE && typeof event.id === "string"; +}