945 lines
40 KiB
HTML
945 lines
40 KiB
HTML
{% load i18n %}
|
|
{% get_current_language as LANGUAGE_CODE %}
|
|
|
|
<script data-id="nodedc-auth-favicon">
|
|
"use strict";
|
|
|
|
(function () {
|
|
const faviconHref = "data:image/svg+xml,%3Csvg%20width%3D%2232%22%20height%3D%2232%22%20viewBox%3D%220%200%2032%2032%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cstyle%3E%0A%20%20%20%20.icon-fav-01%20%7B%20fill%3A%20%23333334%3B%20%7D%0A%20%20%20%20%40media%20(prefers-color-scheme%3A%20dark)%20%7B%20.icon-fav-01%20%7B%20fill%3A%20%23FEFEFE%3B%20%7D%20%7D%0A%20%20%20%20%40media%20(prefers-color-scheme%3A%20light)%20%7B%20.icon-fav-01%20%7B%20fill%3A%20%23000000%3B%20%7D%20%7D%0A%20%20%3C%2Fstyle%3E%0A%20%20%3Cpath%20class%3D%22icon-fav-01%22%20d%3D%22M17.8738%2015.9159L16.0002%2018.9718L14.1267%2015.9159H17.8738ZM23.6307%2012.7864H8.36914L15.9999%2025.2308L23.6307%2012.7864Z%22%2F%3E%0A%20%20%3Cpath%20class%3D%22icon-fav-01%22%20d%3D%22M10.9793%2019.4872L6.67269%2011.5918H25.5344L21.2281%2019.4872H25.0581L31.2614%208H0.738281L7.16244%2019.4872H10.9793Z%22%2F%3E%0A%3C%2Fsvg%3E";
|
|
const applyFavicon = () => {
|
|
if (!document.head) return;
|
|
document.querySelectorAll('link[rel~="icon"]').forEach((node) => node.remove());
|
|
const link = document.createElement("link");
|
|
link.rel = "icon";
|
|
link.type = "image/svg+xml";
|
|
link.sizes = "any";
|
|
link.href = faviconHref;
|
|
document.head.appendChild(link);
|
|
};
|
|
applyFavicon();
|
|
window.addEventListener("DOMContentLoaded", applyFavicon, { once: true });
|
|
})();
|
|
</script>
|
|
|
|
{% with request_host=request.get_host %}
|
|
{% if request_host|slice:":11" != "auth-admin." and request_host|slice:":9" != "id-admin." and request_host|slice:":12" != "172.22.0.222" %}
|
|
{% if request.path|slice:":9" == "/if/flow/" or request.path|slice:":7" == "/flows/" or request.path|slice:":7" == "/login/" %}
|
|
<script data-id="nodedc-auth-flow-scope">
|
|
"use strict";
|
|
|
|
(function () {
|
|
const isAuthentikAdminHost =
|
|
window.location.hostname.startsWith("auth-admin.") ||
|
|
window.location.hostname.startsWith("id-admin.") ||
|
|
window.location.hostname === "172.22.0.222";
|
|
const isAuthExperiencePage =
|
|
!isAuthentikAdminHost &&
|
|
(
|
|
window.location.pathname.includes("/if/flow/") ||
|
|
window.location.pathname.includes("/flows/") ||
|
|
window.location.pathname.includes("/login/")
|
|
);
|
|
document.documentElement.classList.toggle("nodedc-auth-flow-page", isAuthExperiencePage);
|
|
|
|
window.addEventListener("DOMContentLoaded", function () {
|
|
document.body?.classList.toggle("nodedc-auth-flow-page", isAuthExperiencePage);
|
|
}, { once: true });
|
|
})();
|
|
</script>
|
|
|
|
<style data-id="nodedc-auth-login-css">
|
|
{% include "branding/nodedc-login.css" %}
|
|
</style>
|
|
|
|
<style data-id="nodedc-auth-critical-loader">
|
|
html.nodedc-auth-flow-page {
|
|
--ak-global--primary: rgb(195, 255, 102) !important;
|
|
--ak-global--background: #0e0f10 !important;
|
|
--ak-global--background-image: none !important;
|
|
--pf-global--primary-color--100: rgb(195, 255, 102) !important;
|
|
--pf-v5-global--primary-color--100: rgb(195, 255, 102) !important;
|
|
--pf-v6-global--primary-color--100: rgb(195, 255, 102) !important;
|
|
--pf-v4-global--palette--blue-300: rgb(195, 255, 102) !important;
|
|
--ak-global--palette--blue-300: rgb(195, 255, 102) !important;
|
|
--pf-global--active-color--100: rgb(195, 255, 102) !important;
|
|
--pf-global--link--Color: rgb(195, 255, 102) !important;
|
|
--nodedc-auth-primary: rgb(195, 255, 102) !important;
|
|
--nodedc-auth-bg: #0e0f10 !important;
|
|
}
|
|
|
|
html.nodedc-auth-flow-page,
|
|
html.nodedc-auth-flow-page body,
|
|
html.nodedc-auth-flow-page body .pf-c-login {
|
|
background: #0e0f10 !important;
|
|
color-scheme: dark !important;
|
|
}
|
|
|
|
html.nodedc-auth-flow-page body ak-flow-executor::part(main),
|
|
html.nodedc-auth-flow-page body .pf-c-login__main,
|
|
html.nodedc-auth-flow-page body .pf-c-empty-state,
|
|
html.nodedc-auth-flow-page body .pf-v5-c-empty-state,
|
|
html.nodedc-auth-flow-page body .pf-v6-c-empty-state,
|
|
html.nodedc-auth-flow-page body .pf-c-card,
|
|
html.nodedc-auth-flow-page body .pf-v5-c-card,
|
|
html.nodedc-auth-flow-page body .pf-v6-c-card,
|
|
html.nodedc-auth-flow-page body [class*="empty-state"] {
|
|
background: transparent !important;
|
|
box-shadow: none !important;
|
|
border: 0 !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
backdrop-filter: none !important;
|
|
}
|
|
|
|
html.nodedc-auth-flow-page body .pf-c-spinner,
|
|
html.nodedc-auth-flow-page body .pf-v5-c-spinner,
|
|
html.nodedc-auth-flow-page body .pf-v6-c-spinner,
|
|
html.nodedc-auth-flow-page body [role="progressbar"],
|
|
html.nodedc-auth-flow-page body ak-spinner {
|
|
--pf-c-spinner--Color: rgb(195, 255, 102) !important;
|
|
--pf-v5-c-spinner--Color: rgb(195, 255, 102) !important;
|
|
--pf-v6-c-spinner--Color: rgb(195, 255, 102) !important;
|
|
--pf-c-spinner__clipper--after--BoxShadowColor: rgb(195, 255, 102) !important;
|
|
--pf-c-spinner__lead-ball--after--BackgroundColor: rgb(195, 255, 102) !important;
|
|
--pf-c-spinner__tail-ball--after--BackgroundColor: rgb(195, 255, 102) !important;
|
|
color: rgb(195, 255, 102) !important;
|
|
}
|
|
|
|
html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder,
|
|
html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"] {
|
|
position: fixed !important;
|
|
top: 50% !important;
|
|
left: 50% !important;
|
|
display: block !important;
|
|
width: 2.5rem !important;
|
|
height: 2.5rem !important;
|
|
min-width: 2.5rem !important;
|
|
min-height: 2.5rem !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
border: 0 !important;
|
|
border-radius: 999px !important;
|
|
background: transparent !important;
|
|
box-shadow: none !important;
|
|
opacity: 1 !important;
|
|
visibility: visible !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
backdrop-filter: none !important;
|
|
transform: translate(-50%, -50%) !important;
|
|
overflow: visible !important;
|
|
}
|
|
|
|
html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder .pf-c-spinner,
|
|
html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"] .pf-c-spinner,
|
|
html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder [role="progressbar"],
|
|
html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"] [role="progressbar"] {
|
|
opacity: 0 !important;
|
|
visibility: hidden !important;
|
|
}
|
|
|
|
html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder::before,
|
|
html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"]::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
border: 2px solid rgba(195, 255, 102, 0.22);
|
|
border-top-color: rgb(195, 255, 102);
|
|
border-radius: 999px;
|
|
animation: nodedc-auth-placeholder-spin 0.72s linear infinite;
|
|
pointer-events: none;
|
|
}
|
|
|
|
html.nodedc-auth-flow-page body.nodedc-auth-loading ak-flow-executor::part(main),
|
|
html.nodedc-auth-flow-page body.nodedc-auth-loading .pf-c-login__main {
|
|
opacity: 0 !important;
|
|
visibility: hidden !important;
|
|
pointer-events: none !important;
|
|
}
|
|
|
|
html.nodedc-auth-flow-page body.nodedc-auth-loading::before {
|
|
content: "";
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
z-index: 1200;
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
border: 2px solid rgba(195, 255, 102, 0.22);
|
|
border-top-color: rgb(195, 255, 102);
|
|
border-radius: 999px;
|
|
transform: translate(-50%, -50%);
|
|
animation: nodedc-auth-loader-spin 0.72s linear infinite;
|
|
pointer-events: none;
|
|
}
|
|
|
|
html.nodedc-auth-flow-page body.nodedc-auth-card-ready::before {
|
|
display: none !important;
|
|
}
|
|
|
|
@keyframes nodedc-auth-loader-spin {
|
|
from {
|
|
transform: translate(-50%, -50%) rotate(0deg);
|
|
}
|
|
|
|
to {
|
|
transform: translate(-50%, -50%) rotate(360deg);
|
|
}
|
|
}
|
|
|
|
@keyframes nodedc-auth-placeholder-spin {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<script data-id="authentik-config">
|
|
"use strict";
|
|
|
|
const isAuthentikAdminHost =
|
|
window.location.hostname.startsWith("auth-admin.") ||
|
|
window.location.hostname.startsWith("id-admin.") ||
|
|
window.location.hostname === "172.22.0.222";
|
|
const isAuthExperiencePage =
|
|
!isAuthentikAdminHost &&
|
|
(
|
|
window.location.pathname.includes("/if/flow/") ||
|
|
window.location.pathname.includes("/flows/") ||
|
|
window.location.pathname.includes("/login/")
|
|
);
|
|
const authentikBrand = JSON.parse('{{ brand_json|escapejs }}' || "{}");
|
|
if (isAuthExperiencePage) {
|
|
const nodedcAuthLoginCss = document.querySelector('style[data-id="nodedc-auth-login-css"]')?.textContent || "";
|
|
authentikBrand.branding_custom_css = nodedcAuthLoginCss;
|
|
authentikBrand.brandingCustomCss = nodedcAuthLoginCss;
|
|
} else {
|
|
authentikBrand.branding_custom_css = "";
|
|
authentikBrand.brandingCustomCss = "";
|
|
}
|
|
|
|
window.authentik = {
|
|
locale: "ru",
|
|
config: JSON.parse('{{ config_json|escapejs }}' || "{}"),
|
|
brand: authentikBrand,
|
|
versionFamily: "{{ version_family }}",
|
|
versionSubdomain: "{{ version_subdomain }}",
|
|
build: "{{ build }}",
|
|
api: {
|
|
base: "{{ base_url }}",
|
|
relBase: "{{ base_url_rel }}",
|
|
},
|
|
};
|
|
window.addEventListener("DOMContentLoaded", function () {
|
|
{% for message in messages %}
|
|
window.dispatchEvent(
|
|
new CustomEvent("ak-message", {
|
|
bubbles: true,
|
|
composed: true,
|
|
detail: {
|
|
level: "{{ message.tags|escapejs }}",
|
|
message: "{{ message.message|escapejs }}",
|
|
},
|
|
}),
|
|
);
|
|
{% endfor %}
|
|
});
|
|
</script>
|
|
|
|
{% with request_host=request.get_host %}
|
|
{% if request_host|slice:":11" != "auth-admin." and request_host|slice:":9" != "id-admin." and request_host|slice:":12" != "172.22.0.222" %}
|
|
{% if request.path|slice:":9" == "/if/flow/" or request.path|slice:":7" == "/flows/" or request.path|slice:":7" == "/login/" %}
|
|
<script data-id="nodedc-auth-field-enhancements">
|
|
"use strict";
|
|
|
|
(function () {
|
|
const isAuthentikAdminHost =
|
|
window.location.hostname.startsWith("auth-admin.") ||
|
|
window.location.hostname.startsWith("id-admin.") ||
|
|
window.location.hostname === "172.22.0.222";
|
|
const isAuthExperiencePage =
|
|
!isAuthentikAdminHost &&
|
|
(
|
|
window.location.pathname.includes("/if/flow/") ||
|
|
window.location.pathname.includes("/flows/") ||
|
|
window.location.pathname.includes("/login/")
|
|
);
|
|
if (!isAuthExperiencePage) return;
|
|
|
|
document.documentElement.classList.add("nodedc-auth-flow-page");
|
|
window.addEventListener("DOMContentLoaded", function () {
|
|
document.body?.classList.add("nodedc-auth-flow-page");
|
|
}, { once: true });
|
|
|
|
const logoSvg = `
|
|
<svg id="nodedc-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220.82 54.55" aria-hidden="true">
|
|
<defs>
|
|
<style>
|
|
.cls-1{fill:#e2e1e1;}
|
|
.cls-2{fill:#dbdbdb;stroke:#dbdbdb;stroke-miterlimit:10;stroke-width:0.75px;}
|
|
</style>
|
|
</defs>
|
|
<path class="cls-1" d="M52.8,23.61,46.92,33.76,41.05,23.61H52.8m18-10.39H23.06L46.92,54.55Z"></path>
|
|
<polygon class="cls-1" points="31.28 33.13 18.11 10.34 75.73 10.34 62.59 33.13 74.28 33.13 93.22 0 0 0 19.61 33.13 31.28 33.13"></polygon>
|
|
<path class="cls-2" d="M116.35,18.49V1h1.27l10.34,15V1h1.33V18.49H128l-10.34-15v15Z"></path>
|
|
<path class="cls-2" d="M140.43,18.64c-4.79,0-8.16-3.72-8.16-8.89S135.64.86,140.43.86s8.17,3.72,8.17,8.89S145.25,18.64,140.43,18.64Zm0-1.25c4,0,6.79-3.17,6.79-7.64s-2.77-7.64-6.79-7.64-6.77,3.17-6.77,7.64S136.44,17.39,140.43,17.39Z"></path>
|
|
<path class="cls-2" d="M151.6,18.49V1h5.1c5.54,0,8.79,3.42,8.79,8.74s-3.25,8.74-8.79,8.74ZM153,17.24h3.75c4.77,0,7.42-2.92,7.42-7.49s-2.65-7.49-7.42-7.49H153Z"></path>
|
|
<path class="cls-2" d="M168.49,1h10.77V2.26h-9.42V8.93h7.89v1.25h-7.89v7.06h9.74v1.25H168.49Z"></path>
|
|
<path class="cls-2" d="M188.88,18.49V1H194c5.54,0,8.79,3.42,8.79,8.74s-3.25,8.74-8.79,8.74Zm1.35-1.25H194c4.77,0,7.41-2.92,7.41-7.49S198.75,2.26,194,2.26h-3.75Z"></path>
|
|
<path class="cls-2" d="M205.15,9.75c0-5.24,3.19-8.89,8.11-8.89a6.8,6.8,0,0,1,7.1,5.52h-1.43a5.54,5.54,0,0,0-5.74-4.27c-4.05,0-6.64,3.17-6.64,7.64s2.54,7.64,6.59,7.64a5.46,5.46,0,0,0,5.74-4.29h1.43c-.75,3.52-3.4,5.54-7.15,5.54C208.27,18.64,205.15,15.05,205.15,9.75Z"></path>
|
|
</svg>
|
|
`;
|
|
const clearIcon = `
|
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"></circle>
|
|
<path d="M9 9l6 6M15 9l-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
|
|
</svg>
|
|
`;
|
|
const genericLoginError = "Не удалось выполнить вход. Проверьте email и пароль или запросите доступ.";
|
|
const revokedLoginError = "Аккаунт больше не активен. Запросите доступ, если хотите подключиться снова.";
|
|
const genericFlowErrorMessages = [
|
|
"Response returned an error code",
|
|
"Response returned an error",
|
|
"Не удалось завершить операцию.",
|
|
];
|
|
const accountStatusCache = new Map();
|
|
const authTranslations = new Map([
|
|
["Failed to authenticate.", genericLoginError],
|
|
["Invalid credentials.", genericLoginError],
|
|
["Invalid password.", genericLoginError],
|
|
["Incorrect password.", genericLoginError],
|
|
["Authentication failed.", genericLoginError],
|
|
["This field is required.", "Заполните это поле."],
|
|
["Please enter your password", "Введите пароль"],
|
|
["Please enter your password.", "Введите пароль."],
|
|
["Please enter a valid email address.", "Введите корректную электронную почту."],
|
|
["Caps Lock is enabled.", "Включён Caps Lock."],
|
|
["Forgot username or password?", "Забыли пароль?"],
|
|
["Forgot password?", "Забыли пароль?"],
|
|
["Remember me on this device", "Запомнить на этом устройстве"],
|
|
["Email", "Эл. почта"],
|
|
["Email or Username", "Эл. почта"],
|
|
["Password", "Пароль"],
|
|
["Log in", "Войти"],
|
|
["Continue", "Продолжить"],
|
|
["Show password", "Показать пароль"],
|
|
["Hide password", "Скрыть пароль"],
|
|
["Permission denied", "Доступ запрещён"],
|
|
["Not you?", "Сменить пользователя"],
|
|
["Request has been denied.", "Доступ запрещён."],
|
|
["The request has been denied.", "Доступ запрещён."],
|
|
["В произведении запроса было отказано.", "Доступ запрещён."],
|
|
["Go home", "Вернуться назад"],
|
|
["Response returned an error code", "Не удалось завершить операцию."],
|
|
]);
|
|
|
|
function visitRoots(root, callback) {
|
|
callback(root);
|
|
root.querySelectorAll("*").forEach((element) => {
|
|
if (element.shadowRoot) {
|
|
visitRoots(element.shadowRoot, callback);
|
|
}
|
|
});
|
|
}
|
|
|
|
function ensureLogo() {
|
|
if (!document.body || document.querySelector("[data-nodedc-auth-logo='true']")) return;
|
|
|
|
document.body.classList.add("nodedc-auth-enhanced");
|
|
const logo = document.createElement("a");
|
|
logo.className = "nodedc-auth-logo";
|
|
logo.dataset.nodedcAuthLogo = "true";
|
|
logo.href = getLauncherBaseUrl();
|
|
logo.setAttribute("aria-label", "NODE.DC");
|
|
logo.innerHTML = logoSvg;
|
|
document.body.appendChild(logo);
|
|
updateLogoLink(logo);
|
|
}
|
|
|
|
function getLauncherBaseUrl() {
|
|
const hostname = window.location.hostname;
|
|
const launcherHostnames = {
|
|
"id.nodedc.ru": "hub.nodedc.ru",
|
|
"auth.nodedc.ru": "hub.nodedc.ru",
|
|
"id.notdc.ru": "launcher.notdc.ru",
|
|
"auth.notdc.ru": "launcher.notdc.ru",
|
|
};
|
|
const launcherHostname =
|
|
launcherHostnames[hostname] ||
|
|
(hostname.startsWith("auth.")
|
|
? `launcher.${hostname.slice(5)}`
|
|
: hostname.startsWith("id.")
|
|
? `hub.${hostname.slice(3)}`
|
|
: "hub.nodedc.ru");
|
|
const port = window.location.port ? `:${window.location.port}` : "";
|
|
|
|
return `${window.location.protocol}//${launcherHostname}${port}/`;
|
|
}
|
|
|
|
function updateLogoLink(logo) {
|
|
fetch(new URL("/api/public/brand", getLauncherBaseUrl()).toString(), { cache: "no-store" })
|
|
.then((response) => (response.ok ? response.json() : null))
|
|
.then((payload) => {
|
|
if (payload?.logoLinkUrl) {
|
|
logo.href = payload.logoLinkUrl;
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
function translateTextValue(value) {
|
|
if (!value) return value;
|
|
|
|
const trimmed = value.trim();
|
|
const translated = authTranslations.get(trimmed);
|
|
if (!translated) return value;
|
|
|
|
return value.replace(trimmed, translated);
|
|
}
|
|
|
|
function translateAuthText(root) {
|
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
acceptNode(node) {
|
|
const parent = node.parentElement;
|
|
if (!parent || !node.nodeValue?.trim()) return NodeFilter.FILTER_REJECT;
|
|
if (["SCRIPT", "STYLE", "SVG", "PATH", "POLYGON"].includes(parent.tagName)) {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
},
|
|
});
|
|
|
|
const textNodes = [];
|
|
while (walker.nextNode()) {
|
|
textNodes.push(walker.currentNode);
|
|
}
|
|
|
|
textNodes.forEach((node) => {
|
|
const translated = translateTextValue(node.nodeValue);
|
|
if (translated !== node.nodeValue) {
|
|
node.nodeValue = translated;
|
|
}
|
|
});
|
|
|
|
root.querySelectorAll("[aria-label], [title], [placeholder], input[value], button").forEach((element) => {
|
|
["aria-label", "title", "placeholder"].forEach((attribute) => {
|
|
const value = element.getAttribute(attribute);
|
|
const translated = translateTextValue(value);
|
|
if (translated !== value) element.setAttribute(attribute, translated);
|
|
});
|
|
});
|
|
}
|
|
|
|
function hasPermissionDeniedScreen() {
|
|
let denied = false;
|
|
visitRoots(document, (root) => {
|
|
if (denied) return;
|
|
const text = root.textContent || "";
|
|
denied = [
|
|
"Permission denied",
|
|
"Доступ ограничен",
|
|
"Доступ запрещен",
|
|
"Доступ запрещён",
|
|
"Request has been denied.",
|
|
"The request has been denied.",
|
|
"В произведении запроса было отказано.",
|
|
"Доступ к модулю ограничен.",
|
|
].some((message) => text.includes(message));
|
|
});
|
|
return denied;
|
|
}
|
|
|
|
function syncPermissionDeniedState() {
|
|
document.body?.classList.toggle("nodedc-auth-permission-denied", hasPermissionDeniedScreen());
|
|
}
|
|
|
|
function findAncestor(element, predicate, maxDepth = 8) {
|
|
let current = element;
|
|
for (let depth = 0; current && depth < maxDepth; depth += 1) {
|
|
if (predicate(current)) return current;
|
|
current = current.parentElement;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function applyPermissionDeniedLayout(root) {
|
|
const deniedTitleMessages = ["Permission denied", "Доступ ограничен", "Доступ запрещен", "Доступ запрещён"];
|
|
const deniedSubtitle = "Доступ запрещён.";
|
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
acceptNode(node) {
|
|
const text = node.nodeValue?.trim() || "";
|
|
return deniedTitleMessages.some((message) => text === message)
|
|
? NodeFilter.FILTER_ACCEPT
|
|
: NodeFilter.FILTER_REJECT;
|
|
},
|
|
});
|
|
|
|
const textNodes = [];
|
|
while (walker.nextNode()) {
|
|
textNodes.push(walker.currentNode);
|
|
}
|
|
|
|
textNodes.forEach((node) => {
|
|
const titleElement = findAncestor(
|
|
node.parentElement,
|
|
(element) =>
|
|
element.matches?.(".pf-c-title, .pf-v5-c-title, h1, h2") ||
|
|
element.getAttribute?.("slot") === "title",
|
|
);
|
|
const header =
|
|
titleElement?.closest?.(".pf-c-login__main-header") ||
|
|
findAncestor(titleElement || node.parentElement, (element) => element.matches?.(".pf-c-login__main-header"), 10) ||
|
|
root.querySelector?.(".pf-c-login__main-header");
|
|
|
|
if (titleElement) {
|
|
titleElement.style.display = "none";
|
|
}
|
|
if (header) {
|
|
header.setAttribute("data-nodedc-permission-denied", "true");
|
|
header.setAttribute("data-nodedc-denied-subtitle", deniedSubtitle);
|
|
}
|
|
});
|
|
}
|
|
|
|
function hidePermissionDeniedReason(root) {
|
|
const deniedMessages = [
|
|
"Request has been denied.",
|
|
"The request has been denied.",
|
|
"В произведении запроса было отказано.",
|
|
"Доступ к модулю ограничен.",
|
|
"Объяснение:",
|
|
"Reason:",
|
|
"Привязка политики",
|
|
"Policy binding",
|
|
"returned result",
|
|
"returned a result",
|
|
"'None' вернула результат",
|
|
];
|
|
|
|
root.querySelectorAll(".pf-c-alert, .pf-v5-c-alert, .pf-c-empty-state__body, [class*='alert']").forEach((element) => {
|
|
const text = element.textContent || "";
|
|
if (deniedMessages.some((message) => text.includes(message))) {
|
|
element.style.display = "none";
|
|
}
|
|
});
|
|
|
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
acceptNode(node) {
|
|
const text = node.nodeValue?.trim() || "";
|
|
return deniedMessages.some((message) => text.includes(message))
|
|
? NodeFilter.FILTER_ACCEPT
|
|
: NodeFilter.FILTER_REJECT;
|
|
},
|
|
});
|
|
|
|
const textNodes = [];
|
|
while (walker.nextNode()) {
|
|
textNodes.push(walker.currentNode);
|
|
}
|
|
|
|
textNodes.forEach((node) => {
|
|
let element = node.parentElement;
|
|
for (let depth = 0; element && depth < 6; depth += 1) {
|
|
const text = (element.textContent || "").trim();
|
|
if (deniedMessages.some((message) => text === message || text.endsWith(message)) && text.length <= 96) {
|
|
element.style.display = "none";
|
|
return;
|
|
}
|
|
element = element.parentElement;
|
|
}
|
|
if (node.parentElement) {
|
|
node.parentElement.style.display = "none";
|
|
}
|
|
});
|
|
}
|
|
|
|
function enhanceIdentifierField(root) {
|
|
const input = root.querySelector("#ak-identifier-input");
|
|
if (!input || input.dataset.nodedcClearBound === "true") return;
|
|
|
|
const fieldGroup = input.closest(".pf-c-form__group") || input.parentElement;
|
|
if (!fieldGroup) return;
|
|
|
|
input.dataset.nodedcClearBound = "true";
|
|
fieldGroup.style.position = "relative";
|
|
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.tabIndex = -1;
|
|
button.className = "nodedc-auth-clear-input";
|
|
button.dataset.nodedcClearInput = "true";
|
|
button.setAttribute("aria-label", "Очистить электронную почту");
|
|
button.innerHTML = clearIcon;
|
|
|
|
const update = () => {
|
|
button.dataset.visible = input.value ? "true" : "false";
|
|
};
|
|
|
|
button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
input.value = "";
|
|
input.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
input.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
input.focus();
|
|
update();
|
|
});
|
|
|
|
input.addEventListener("input", update);
|
|
input.addEventListener("change", update);
|
|
fieldGroup.appendChild(button);
|
|
update();
|
|
}
|
|
|
|
function maskPasswordInput(input) {
|
|
if (!input || input.type !== "text") return;
|
|
|
|
input.type = "password";
|
|
const button = input.closest(".pf-c-input-group")?.querySelector(".pf-c-button.pf-m-control");
|
|
const icon = button?.querySelector("i");
|
|
|
|
button?.setAttribute("aria-label", "Показать пароль");
|
|
icon?.classList.remove("fa-eye-slash");
|
|
icon?.classList.add("fa-eye");
|
|
}
|
|
|
|
function maskVisiblePasswords(target) {
|
|
visitRoots(document, (root) => {
|
|
root.querySelectorAll("#ak-stage-identification-password, #ak-stage-password-input").forEach((input) => {
|
|
const inputGroup = input.closest(".pf-c-input-group");
|
|
if (inputGroup?.contains(target)) return;
|
|
maskPasswordInput(input);
|
|
});
|
|
});
|
|
}
|
|
|
|
function enhancePasswordFields(root) {
|
|
root.querySelectorAll("#ak-stage-identification-password, #ak-stage-password-input").forEach((input) => {
|
|
if (input.placeholder !== "Введите пароль") {
|
|
input.placeholder = "Введите пароль";
|
|
}
|
|
|
|
if (input.dataset.nodedcBlurBound === "true") return;
|
|
|
|
input.dataset.nodedcBlurBound = "true";
|
|
input.addEventListener("blur", () => {
|
|
window.setTimeout(() => {
|
|
const inputGroup = input.closest(".pf-c-input-group");
|
|
if (inputGroup?.contains(document.activeElement)) return;
|
|
maskPasswordInput(input);
|
|
}, 0);
|
|
});
|
|
});
|
|
}
|
|
|
|
function normalizePasswordFields(root) {
|
|
enhancePasswordFields(root);
|
|
}
|
|
|
|
function dispatchSyntheticInput(input) {
|
|
if (!input) return;
|
|
|
|
try {
|
|
input.dispatchEvent(new InputEvent("input", {
|
|
bubbles: true,
|
|
composed: true,
|
|
inputType: "insertReplacementText",
|
|
data: input.value || "",
|
|
}));
|
|
} catch {
|
|
input.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
}
|
|
input.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
}
|
|
|
|
function syncFormFieldsBeforeSubmit(form) {
|
|
form.querySelectorAll("#ak-identifier-input, #ak-stage-identification-password, #ak-stage-password-input").forEach((input) => {
|
|
dispatchSyntheticInput(input);
|
|
});
|
|
}
|
|
|
|
function enhanceSubmitHandoff(root) {
|
|
root.querySelectorAll("form").forEach((form) => {
|
|
if (form.dataset.nodedcSubmitBound === "true") return;
|
|
|
|
form.dataset.nodedcSubmitBound = "true";
|
|
form.addEventListener("submit", () => {
|
|
syncFormFieldsBeforeSubmit(form);
|
|
document.body?.classList.add("nodedc-auth-submitting");
|
|
}, true);
|
|
});
|
|
}
|
|
|
|
function ensureAccessRequestLink(root) {
|
|
const input = root.querySelector("#ak-identifier-input, #ak-stage-identification-password, #ak-stage-password-input");
|
|
const form = input?.closest("form") || root.querySelector("form");
|
|
if (!input || !form || form.querySelector("[data-nodedc-access-request='true']")) return;
|
|
|
|
const link = document.createElement("a");
|
|
link.className = "nodedc-auth-request-access";
|
|
link.dataset.nodedcAccessRequest = "true";
|
|
link.href = new URL("/request-access", getLauncherBaseUrl()).toString();
|
|
link.textContent = "Запросить доступ";
|
|
form.appendChild(link);
|
|
}
|
|
|
|
function normalizeEmail(value) {
|
|
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
}
|
|
|
|
function getLoginEmail(root) {
|
|
return normalizeEmail(root.querySelector("#ak-identifier-input")?.value);
|
|
}
|
|
|
|
function getLoginErrorMessages() {
|
|
return [
|
|
"Failed to authenticate.",
|
|
"Invalid credentials.",
|
|
"Invalid password.",
|
|
"Incorrect password.",
|
|
"Authentication failed.",
|
|
"Не удалось выполнить вход.",
|
|
"Неверная почта или пароль.",
|
|
"Неверный пароль.",
|
|
genericLoginError,
|
|
revokedLoginError,
|
|
];
|
|
}
|
|
|
|
function replaceLoginErrorText(root, message) {
|
|
const errorMessages = getLoginErrorMessages();
|
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
acceptNode(node) {
|
|
const text = node.nodeValue?.trim() || "";
|
|
return errorMessages.some((errorMessage) => text.includes(errorMessage))
|
|
? NodeFilter.FILTER_ACCEPT
|
|
: NodeFilter.FILTER_REJECT;
|
|
},
|
|
});
|
|
const textNodes = [];
|
|
|
|
while (walker.nextNode()) {
|
|
textNodes.push(walker.currentNode);
|
|
}
|
|
|
|
textNodes.forEach((node) => {
|
|
const currentValue = node.nodeValue || "";
|
|
const trimmed = currentValue.trim();
|
|
if (!trimmed) return;
|
|
node.nodeValue = currentValue.replace(trimmed, message);
|
|
});
|
|
}
|
|
|
|
function syncLoginErrorReason(root) {
|
|
if (!hasAuthError(root)) return;
|
|
|
|
replaceLoginErrorText(root, genericLoginError);
|
|
const email = getLoginEmail(root);
|
|
if (!email) return;
|
|
|
|
const cachedStatus = accountStatusCache.get(email);
|
|
if (cachedStatus === "revoked") {
|
|
replaceLoginErrorText(root, revokedLoginError);
|
|
return;
|
|
}
|
|
|
|
if (cachedStatus === "unknown" || cachedStatus === "loading") return;
|
|
|
|
accountStatusCache.set(email, "loading");
|
|
const statusUrl = new URL("/api/public/login-account-status", getLauncherBaseUrl());
|
|
statusUrl.searchParams.set("email", email);
|
|
|
|
fetch(statusUrl.toString(), { cache: "no-store" })
|
|
.then((response) => (response.ok ? response.json() : null))
|
|
.then((payload) => {
|
|
const status = payload?.status === "revoked" ? "revoked" : "unknown";
|
|
accountStatusCache.set(email, status);
|
|
if (status !== "revoked") return;
|
|
visitRoots(document, (candidateRoot) => {
|
|
replaceLoginErrorText(candidateRoot, revokedLoginError);
|
|
});
|
|
})
|
|
.catch(() => {
|
|
accountStatusCache.set(email, "unknown");
|
|
});
|
|
}
|
|
|
|
function hasAuthError(root) {
|
|
const hasErrorElement = root.querySelector(
|
|
".pf-c-alert, .pf-m-error, .pf-c-helper-text__item.pf-m-error, [aria-invalid='true']"
|
|
);
|
|
if (hasErrorElement) return true;
|
|
|
|
const text = root.textContent || "";
|
|
return getLoginErrorMessages().some((message) => text.includes(message));
|
|
}
|
|
|
|
function hasGenericFlowError(root) {
|
|
const text = root.textContent || "";
|
|
return genericFlowErrorMessages.some((message) => text.includes(message));
|
|
}
|
|
|
|
function hasLoadingOnlyScreen(root) {
|
|
const isFlowExecutorRoot = root.host?.tagName?.toLowerCase() === "ak-flow-executor";
|
|
const hasProgress =
|
|
root.querySelector?.(".pf-c-spinner, .pf-v5-c-spinner, [role='progressbar'], ak-spinner");
|
|
const isEmptyFlowExecutor = isFlowExecutorRoot && (root.textContent || "").trim() === "";
|
|
const hasInteractiveAuth =
|
|
root.querySelector?.("input, textarea, select, button[type='submit'], .pf-c-alert, .pf-v5-c-alert") ||
|
|
hasGenericFlowError(root);
|
|
|
|
return Boolean((hasProgress || isEmptyFlowExecutor) && !hasInteractiveAuth);
|
|
}
|
|
|
|
function syncLoadingState() {
|
|
let isLoadingOnly = false;
|
|
let hasAuthContent = false;
|
|
visitRoots(document, (root) => {
|
|
if (hasLoadingOnlyScreen(root)) {
|
|
isLoadingOnly = true;
|
|
}
|
|
if (
|
|
root.querySelector?.("input, textarea, select, button[type='submit'], .pf-c-alert, .pf-v5-c-alert") ||
|
|
hasGenericFlowError(root)
|
|
) {
|
|
hasAuthContent = true;
|
|
}
|
|
});
|
|
const shouldShowLoader = isLoadingOnly && !hasAuthContent;
|
|
|
|
document.body?.classList.toggle("nodedc-auth-loading", shouldShowLoader);
|
|
document.body?.classList.toggle("nodedc-auth-card-ready", hasAuthContent);
|
|
}
|
|
|
|
function restoreCardOnErrors(root) {
|
|
if (hasAuthError(root)) {
|
|
document.body?.classList.remove("nodedc-auth-submitting");
|
|
}
|
|
}
|
|
|
|
function isOidcLogoutFlow() {
|
|
const path = window.location.pathname;
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
return (
|
|
path.includes("/end-session/") ||
|
|
path.includes("/if/flow/default-invalidation-flow") ||
|
|
params.has("post_logout_redirect_uri")
|
|
);
|
|
}
|
|
|
|
function getSafePostLogoutRedirect() {
|
|
const rawUrl = new URLSearchParams(window.location.search).get("post_logout_redirect_uri");
|
|
if (!rawUrl) return null;
|
|
|
|
try {
|
|
const url = new URL(rawUrl);
|
|
const allowedHosts = new Set([
|
|
"hub.nodedc.ru",
|
|
"hub.notdc.ru",
|
|
"launcher.notdc.ru",
|
|
"platform.notdc.ru",
|
|
"notdc.ru",
|
|
]);
|
|
|
|
if (!["http:", "https:"].includes(url.protocol)) return null;
|
|
if (!allowedHosts.has(url.hostname)) return null;
|
|
if (!url.pathname.startsWith("/auth/logged-out")) return null;
|
|
|
|
return url.toString();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function buildLoggedOutRedirectUrl() {
|
|
return new URL("/auth/logged-out", getLauncherBaseUrl()).toString();
|
|
}
|
|
|
|
function redirectGenericFlowError(root) {
|
|
if (document.body?.dataset.nodedcFlowErrorRedirected === "true") return;
|
|
if (!window.location.pathname.includes("/if/flow/")) return;
|
|
if (!hasGenericFlowError(root)) return;
|
|
|
|
document.body.dataset.nodedcFlowErrorRedirected = "true";
|
|
document.body.classList.add("nodedc-auth-submitting");
|
|
window.setTimeout(() => {
|
|
window.location.replace(getSafePostLogoutRedirect() || buildLoggedOutRedirectUrl());
|
|
}, 120);
|
|
}
|
|
|
|
function redirectCompletedLogout(root) {
|
|
if (!isOidcLogoutFlow() || document.body?.dataset.nodedcLogoutRedirected === "true") return;
|
|
|
|
const redirectUrl = getSafePostLogoutRedirect();
|
|
if (!redirectUrl) return;
|
|
|
|
const isInvalidationFlow = window.location.pathname.includes("/if/flow/default-invalidation-flow");
|
|
const text = root.textContent || "";
|
|
const logoutComplete = [
|
|
"Logout successful",
|
|
"You've logged out",
|
|
"You have been logged out",
|
|
"Logged out",
|
|
"Вы вышли",
|
|
"Выход выполнен",
|
|
].some((message) => text.includes(message));
|
|
|
|
if (!logoutComplete && !isInvalidationFlow) return;
|
|
|
|
document.body.dataset.nodedcLogoutRedirected = "true";
|
|
document.body.classList.add("nodedc-auth-submitting");
|
|
window.setTimeout(() => {
|
|
window.location.replace(redirectUrl);
|
|
}, isInvalidationFlow ? 1600 : 150);
|
|
}
|
|
|
|
let scheduled = false;
|
|
|
|
function enhanceAuthFields() {
|
|
scheduled = false;
|
|
ensureLogo();
|
|
visitRoots(document, (root) => {
|
|
enhanceIdentifierField(root);
|
|
normalizePasswordFields(root);
|
|
enhanceSubmitHandoff(root);
|
|
ensureAccessRequestLink(root);
|
|
translateAuthText(root);
|
|
syncLoginErrorReason(root);
|
|
applyPermissionDeniedLayout(root);
|
|
hidePermissionDeniedReason(root);
|
|
restoreCardOnErrors(root);
|
|
redirectCompletedLogout(root);
|
|
redirectGenericFlowError(root);
|
|
});
|
|
syncPermissionDeniedState();
|
|
syncLoadingState();
|
|
}
|
|
|
|
function scheduleEnhancement() {
|
|
if (scheduled) return;
|
|
scheduled = true;
|
|
window.requestAnimationFrame(enhanceAuthFields);
|
|
}
|
|
|
|
window.addEventListener("DOMContentLoaded", scheduleEnhancement);
|
|
window.addEventListener("load", scheduleEnhancement);
|
|
document.addEventListener("pointerdown", (event) => {
|
|
const target = event.composedPath?.()[0] || event.target;
|
|
maskVisiblePasswords(target);
|
|
}, true);
|
|
window.addEventListener("blur", () => maskVisiblePasswords(null));
|
|
new MutationObserver(scheduleEnhancement).observe(document.documentElement, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
})();
|
|
</script>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% endwith %}
|