NODEDC_PLATFORM/infra/authentik/custom-templates/base/header_js.html

588 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
<script data-id="authentik-config">
"use strict";
window.authentik = {
locale: "ru",
config: JSON.parse('{{ config_json|escapejs }}' || "{}"),
brand: JSON.parse('{{ brand_json|escapejs }}' || "{}"),
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>
<script data-id="nodedc-auth-field-enhancements">
"use strict";
(function () {
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 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.", "Доступ к модулю ограничен."],
["Go home", "Вернуться назад"],
]);
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 launcherHostname = hostname.startsWith("auth.") ? `launcher.${hostname.slice(5)}` : "launcher.local.nodedc";
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.",
"Доступ к модулю ограничен.",
].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 = "Доступ к модулю NODE.DC ограничен.";
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.", "Доступ к модулю ограничен."];
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 enhanceSubmitHandoff(root) {
root.querySelectorAll("form").forEach((form) => {
if (form.dataset.nodedcSubmitBound === "true") return;
form.dataset.nodedcSubmitBound = "true";
form.addEventListener("submit", () => {
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 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([
"launcher.local.nodedc",
"launcher.local.notdc",
"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 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);
});
syncPermissionDeniedState();
}
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>