ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: устойчивый global logout и скрытие служебных экранов
This commit is contained in:
parent
583f1547e1
commit
09400f7db8
|
|
@ -207,24 +207,6 @@
|
||||||
"coverMediaKind": "image",
|
"coverMediaKind": "image",
|
||||||
"coverMediaSource": "file",
|
"coverMediaSource": "file",
|
||||||
"coverMediaFileName": "1777711943125-691830c2-NODEDC_DT_MMAP.png"
|
"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": [
|
"grants": [
|
||||||
|
|
@ -1140,6 +1122,18 @@
|
||||||
"clientId": null,
|
"clientId": null,
|
||||||
"result": "success",
|
"result": "success",
|
||||||
"details": "Groups: nodedc:launcher:user, nodedc:taskmanager:user, service-digital-twin"
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,7 @@ app.get("/auth/logout", asyncRoute(async (req, res) => {
|
||||||
const session = getCurrentSession(req);
|
const session = getCurrentSession(req);
|
||||||
const returnTo = sanitizeReturnTo(req.query.returnTo);
|
const returnTo = sanitizeReturnTo(req.query.returnTo);
|
||||||
const globalLogout = req.query.global === "1" || req.query.global === "true";
|
const globalLogout = req.query.global === "1" || req.query.global === "true";
|
||||||
|
const taskSessionLogoutPromise = globalLogout ? notifyTaskSessionLogout(session) : Promise.resolve();
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
sessions.delete(session.id);
|
sessions.delete(session.id);
|
||||||
|
|
@ -200,6 +201,7 @@ app.get("/auth/logout", asyncRoute(async (req, res) => {
|
||||||
|
|
||||||
const discovery = await getOidcDiscovery();
|
const discovery = await getOidcDiscovery();
|
||||||
const logoutUrl = buildOidcLogoutUrl(discovery, returnTo, session?.tokenSet.idToken);
|
const logoutUrl = buildOidcLogoutUrl(discovery, returnTo, session?.tokenSet.idToken);
|
||||||
|
await taskSessionLogoutPromise;
|
||||||
|
|
||||||
setNoStore(res);
|
setNoStore(res);
|
||||||
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), logoutUrl.toString()));
|
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), logoutUrl.toString()));
|
||||||
|
|
@ -600,6 +602,9 @@ function readConfig() {
|
||||||
taskLogoutUrl:
|
taskLogoutUrl:
|
||||||
process.env.TASK_LOGOUT_URL ??
|
process.env.TASK_LOGOUT_URL ??
|
||||||
`${(process.env.TASK_BASE_URL ?? `http://${process.env.TASK_DOMAIN ?? "task.local.nodedc"}`).replace(/\/$/, "")}/logout`,
|
`${(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))];
|
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) {
|
function normalizeLogoutUrl(value) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(value);
|
const url = new URL(value);
|
||||||
|
|
@ -958,20 +1001,12 @@ function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Выход из NODE.DC</title>
|
<title>NODE.DC</title>
|
||||||
<style>
|
<style>
|
||||||
html,body{height:100%;margin:0;background:#050606;color:#f4f4f4;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
html,body{height:100%;margin:0;background:#0b0f0e;color:#0b0f0e}
|
||||||
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}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body aria-busy="true">
|
||||||
<main>
|
|
||||||
<h1>Выходим из NODE.DC</h1>
|
|
||||||
<p>Закрываем сессии подключённых приложений и платформенный вход.</p>
|
|
||||||
</main>
|
|
||||||
<script>
|
<script>
|
||||||
const eventPayload = {
|
const eventPayload = {
|
||||||
type: "nodedc:session:logout",
|
type: "nodedc:session:logout",
|
||||||
|
|
@ -995,7 +1030,7 @@ function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
|
||||||
image.referrerPolicy = "no-referrer";
|
image.referrerPolicy = "no-referrer";
|
||||||
image.src = logoutUrl;
|
image.src = logoutUrl;
|
||||||
}
|
}
|
||||||
window.setTimeout(() => window.location.replace(finalRedirectUrl), 1200);
|
window.setTimeout(() => window.location.replace(finalRedirectUrl), 50);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|
|
||||||
|
|
@ -516,7 +516,7 @@ export function LauncherApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authSession) {
|
if (!authSession) {
|
||||||
return <AuthStateScreen title="Проверяем сессию NODE.DC" description="Платформа подготавливает рабочую область и список приложений." />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authSession.authenticated) {
|
if (!authSession.authenticated) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue