ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: устойчивый global logout и скрытие служебных экранов

This commit is contained in:
DCCONSTRUCTIONS 2026-05-05 11:53:48 +03:00
parent 583f1547e1
commit 09400f7db8
3 changed files with 60 additions and 31 deletions

View File

@ -207,24 +207,6 @@
"coverMediaKind": "image",
"coverMediaSource": "file",
"coverMediaFileName": "1777711943125-691830c2-NODEDC_DT_MMAP.png"
},
{
"id": "service_dm",
"slug": "digital-modules",
"title": "Digital Modules",
"subtitle": "Будущие модули",
"description": "Скрытый каталог модулей для root-admin preview.",
"fullDescription": "Площадка для будущих цифровых модулей NODE.DC.",
"url": "https://dm.handhdc.ru/sso/launch",
"launchUrl": "https://dm.handhdc.ru/sso/launch",
"accentColor": "#FF9AC2",
"fallbackGradient": "linear-gradient(135deg, rgba(255, 154, 194, 0.78), rgba(76, 41, 64, 0.9) 44%, #090B0F 86%)",
"status": "hidden",
"order": 60,
"authentikApplicationSlug": "digital-modules",
"authentikGroupName": "service-digital-modules",
"createdAt": "2026-04-10T10:00:00Z",
"updatedAt": "2026-05-01T17:59:10.713Z"
}
],
"grants": [
@ -1140,6 +1122,18 @@
"clientId": null,
"result": "success",
"details": "Groups: nodedc:launcher:user, nodedc:taskmanager:user, service-digital-twin"
},
{
"id": "audit_digital_modules",
"at": "2026-05-04T19:15:22.791Z",
"actorUserId": "user_root",
"actorName": "DC SUDO",
"action": "Удалён сервис",
"objectType": "service",
"objectName": "Digital Modules",
"clientId": null,
"result": "warning",
"details": null
}
]
}

View File

@ -185,6 +185,7 @@ app.get("/auth/logout", asyncRoute(async (req, res) => {
const session = getCurrentSession(req);
const returnTo = sanitizeReturnTo(req.query.returnTo);
const globalLogout = req.query.global === "1" || req.query.global === "true";
const taskSessionLogoutPromise = globalLogout ? notifyTaskSessionLogout(session) : Promise.resolve();
if (session) {
sessions.delete(session.id);
@ -200,6 +201,7 @@ app.get("/auth/logout", asyncRoute(async (req, res) => {
const discovery = await getOidcDiscovery();
const logoutUrl = buildOidcLogoutUrl(discovery, returnTo, session?.tokenSet.idToken);
await taskSessionLogoutPromise;
setNoStore(res);
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), logoutUrl.toString()));
@ -600,6 +602,9 @@ function readConfig() {
taskLogoutUrl:
process.env.TASK_LOGOUT_URL ??
`${(process.env.TASK_BASE_URL ?? `http://${process.env.TASK_DOMAIN ?? "task.local.nodedc"}`).replace(/\/$/, "")}/logout`,
taskInternalLogoutUrl:
process.env.TASK_INTERNAL_LOGOUT_URL ??
`${(process.env.TASK_BASE_URL ?? `http://${process.env.TASK_DOMAIN ?? "task.local.nodedc"}`).replace(/\/$/, "")}/api/internal/nodedc/logout/`,
};
}
@ -939,6 +944,44 @@ function getFrontchannelLogoutUrls() {
return [...new Set(urls.map(normalizeLogoutUrl).filter(Boolean))];
}
async function notifyTaskSessionLogout(session) {
if (!session?.user || !config.internalAccessToken || !config.taskInternalLogoutUrl) {
return;
}
const runtimeContext = getRuntimeSessionContext(session);
const user = runtimeContext.user ?? session.user;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1500);
try {
const response = await fetch(config.taskInternalLogoutUrl, {
method: "POST",
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${config.internalAccessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
source: "launcher-global-logout",
subject: session.user.sub,
email: user.email || session.user.email || null,
}),
signal: controller.signal,
});
if (!response.ok) {
console.warn(`Task internal logout returned ${response.status}`);
}
} catch (error) {
if (error?.name !== "AbortError") {
console.warn(error instanceof Error ? error.message : "Task internal logout failed");
}
} finally {
clearTimeout(timeout);
}
}
function normalizeLogoutUrl(value) {
try {
const url = new URL(value);
@ -958,20 +1001,12 @@ function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Выход из NODE.DC</title>
<title>NODE.DC</title>
<style>
html,body{height:100%;margin:0;background:#050606;color:#f4f4f4;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
body{display:grid;place-items:center}
main{max-width:28rem;padding:2rem;text-align:center}
h1{margin:0 0 .75rem;font-size:1.5rem}
p{margin:0;color:#a6a6a6;line-height:1.5}
html,body{height:100%;margin:0;background:#0b0f0e;color:#0b0f0e}
</style>
</head>
<body>
<main>
<h1>Выходим из NODE.DC</h1>
<p>Закрываем сессии подключённых приложений и платформенный вход.</p>
</main>
<body aria-busy="true">
<script>
const eventPayload = {
type: "nodedc:session:logout",
@ -995,7 +1030,7 @@ function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
image.referrerPolicy = "no-referrer";
image.src = logoutUrl;
}
window.setTimeout(() => window.location.replace(finalRedirectUrl), 1200);
window.setTimeout(() => window.location.replace(finalRedirectUrl), 50);
</script>
</body>
</html>`;

View File

@ -516,7 +516,7 @@ export function LauncherApp() {
}
if (!authSession) {
return <AuthStateScreen title="Проверяем сессию NODE.DC" description="Платформа подготавливает рабочую область и список приложений." />;
return null;
}
if (!authSession.authenticated) {