287 lines
12 KiB
HTML
287 lines
12 KiB
HTML
{% 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220.82 54.55" aria-hidden="true">
|
||
<path fill="currentColor" d="M52.8 23.61 46.92 33.76 41.05 23.61H52.8m18-10.39H23.06l23.86 41.33Z"></path>
|
||
<polygon fill="currentColor" 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 fill="currentColor" d="M116.35 18.49V1h1.27l10.34 15V1h1.33v17.49H128l-10.34-15v15Zm24.08.15c-4.79 0-8.16-3.72-8.16-8.89S135.64.86 140.43.86s8.17 3.72 8.17 8.89-3.35 8.89-8.17 8.89Zm0-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.64 2.78 7.64 6.77 7.64ZM151.6 18.49V1h5.1c5.54 0 8.79 3.42 8.79 8.74s-3.25 8.74-8.79 8.74Zm1.4-1.25h3.75c4.77 0 7.42-2.92 7.42-7.49s-2.65-7.49-7.42-7.49H153ZM168.49 1h10.77v1.26h-9.42v6.67h7.89v1.25h-7.89v7.06h9.74v1.25h-11.09Zm20.39 17.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.75Zm14.92-7.49c0-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.54-4.89 0-8.01-3.59-8.01-8.89Z"></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 authTranslations = new Map([
|
||
["Failed to authenticate.", "Не удалось выполнить вход."],
|
||
["Invalid credentials.", "Неверная почта или пароль."],
|
||
["Invalid password.", "Неверный пароль."],
|
||
["Incorrect password.", "Неверный пароль."],
|
||
["Authentication failed.", "Не удалось выполнить вход."],
|
||
["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", "Скрыть пароль"],
|
||
]);
|
||
|
||
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("div");
|
||
logo.className = "nodedc-auth-logo";
|
||
logo.dataset.nodedcAuthLogo = "true";
|
||
logo.innerHTML = logoSvg;
|
||
document.body.appendChild(logo);
|
||
}
|
||
|
||
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 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 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 [
|
||
"Failed to authenticate.",
|
||
"Не удалось выполнить вход.",
|
||
"Invalid credentials.",
|
||
"Неверная почта или пароль.",
|
||
"Invalid password.",
|
||
"Неверный пароль.",
|
||
].some((message) => text.includes(message));
|
||
}
|
||
|
||
function restoreCardOnErrors(root) {
|
||
if (hasAuthError(root)) {
|
||
document.body?.classList.remove("nodedc-auth-submitting");
|
||
}
|
||
}
|
||
|
||
let scheduled = false;
|
||
|
||
function enhanceAuthFields() {
|
||
scheduled = false;
|
||
ensureLogo();
|
||
visitRoots(document, (root) => {
|
||
enhanceIdentifierField(root);
|
||
normalizePasswordFields(root);
|
||
enhanceSubmitHandoff(root);
|
||
translateAuthText(root);
|
||
restoreCardOnErrors(root);
|
||
});
|
||
}
|
||
|
||
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>
|