285 lines
8.4 KiB
JavaScript
285 lines
8.4 KiB
JavaScript
import { mountWespLogoLoader } from "/static/js/wesp-logo-loader.js?v=6";
|
|
import {
|
|
markLoginTransition,
|
|
storePreloadedApiPayload,
|
|
PRELOAD_FEED_DISPENSERS_KEY,
|
|
} from "/static/js/wesp-nav-preload.js";
|
|
|
|
const LS_LOGIN = "wesp_saved_login";
|
|
const LS_REMEMBER = "wesp_remember_login";
|
|
const THEME_STORAGE_KEY = "theme";
|
|
const POST_LOGIN_REDIRECT_DELAY_MS = 300;
|
|
/** Длительность анимации логотипа после успешного входа (на странице login). */
|
|
const LOGIN_EXIT_ANIM_MS = 3500;
|
|
|
|
const RECIPES_WARM_URLS = [
|
|
"/recipes",
|
|
"/static/css/bootstrap.min.css",
|
|
"/static/css/wesp-zootech-shell.css",
|
|
"/static/css/wesp-zootech-layout.css",
|
|
"/static/css/wesp-zootech-components.css",
|
|
"/static/css/wesp-recipes-skeleton.css",
|
|
"/static/css/wesp-logo-loader.css",
|
|
"/static/css/notyf.min.css",
|
|
"/static/css/font-awesome.min.css",
|
|
"/static/js/wesp-theme-boot.js",
|
|
"/static/js/wesp-logo-loader.js?v=6",
|
|
"/static/js/wesp-page-enter.js",
|
|
"/static/js/wesp-nav-logo-shimmer.js",
|
|
"/static/js/wesp-zootech-nav.js",
|
|
"/static/js/notyf.min.js",
|
|
"/static/js/bootstrap.bundle.min.js",
|
|
"/static/js/wesp-dialog.js",
|
|
"/static/js/pages/recipes-page.js",
|
|
"/static/js/pages/recipes-auth-settings.js",
|
|
"/static/js/pages/recipes-data-controller.js",
|
|
"/static/js/pages/recipes-operations-controller.js",
|
|
"/static/js/pages/recipes-editor-controller.js",
|
|
"/static/js/modules/app-state.js",
|
|
];
|
|
|
|
function delay(ms) {
|
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
}
|
|
|
|
function preloadUrl(url) {
|
|
return fetch(url, { credentials: "same-origin" }).catch(() => null);
|
|
}
|
|
|
|
async function preloadRecipesAfterLogin() {
|
|
markLoginTransition();
|
|
|
|
const apiTask = preloadUrl("/api/feed_dispensers").then(async (response) => {
|
|
if (!response?.ok) return;
|
|
const payload = await response.json();
|
|
if (Array.isArray(payload)) {
|
|
storePreloadedApiPayload(PRELOAD_FEED_DISPENSERS_KEY, payload);
|
|
}
|
|
});
|
|
|
|
await Promise.allSettled([
|
|
apiTask,
|
|
...RECIPES_WARM_URLS.map((url) => preloadUrl(url)),
|
|
]);
|
|
}
|
|
|
|
async function finishLoginTransition(target) {
|
|
const preloadPromise = preloadRecipesAfterLogin();
|
|
await playLoginExitAnimation();
|
|
await Promise.all([preloadPromise, delay(POST_LOGIN_REDIRECT_DELAY_MS)]);
|
|
}
|
|
|
|
function themeForLogoLoader() {
|
|
try {
|
|
const saved = (localStorage.getItem(THEME_STORAGE_KEY) || "").trim().toLowerCase();
|
|
if (saved === "light" || saved === "organic" || saved === "dark") return saved;
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
const attr = document.documentElement.getAttribute("data-theme");
|
|
if (attr === "light" || attr === "organic" || attr === "dark") return attr;
|
|
return "dark";
|
|
}
|
|
|
|
function isDarkTheme() {
|
|
return themeForLogoLoader() === "dark";
|
|
}
|
|
|
|
function safeNextPath(raw) {
|
|
if (raw == null || typeof raw !== "string") return null;
|
|
const s = raw.trim();
|
|
if (!s || s.charAt(0) !== "/") return null;
|
|
if (s.indexOf("//") === 0) return null;
|
|
if (s.indexOf("://") !== -1) return null;
|
|
if (s.indexOf("\0") !== -1 || s.indexOf("\r") !== -1 || s.indexOf("\n") !== -1) return null;
|
|
const pathOnly = s.split("?", 1)[0];
|
|
if (pathOnly.indexOf("@") !== -1) return null;
|
|
return s;
|
|
}
|
|
|
|
function postLoginRedirectUrl() {
|
|
try {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const n = safeNextPath(params.get("next"));
|
|
if (n) return n;
|
|
} catch (e) {
|
|
/* ignore */
|
|
}
|
|
return "/recipes";
|
|
}
|
|
|
|
function showError(message) {
|
|
const errorDiv = document.getElementById("errorMessage");
|
|
if (!errorDiv) return;
|
|
errorDiv.textContent = message;
|
|
errorDiv.style.display = "block";
|
|
window.setTimeout(() => {
|
|
errorDiv.style.display = "none";
|
|
}, 5000);
|
|
}
|
|
|
|
function setLoading(loading) {
|
|
const button = document.getElementById("authButton");
|
|
const buttonText = document.getElementById("buttonText");
|
|
const spinner = document.getElementById("loadingSpinner");
|
|
const loginInput = document.getElementById("loginInput");
|
|
const passwordInput = document.getElementById("passwordInput");
|
|
const rememberInput = document.getElementById("rememberInput");
|
|
|
|
if (button) button.disabled = loading;
|
|
if (loginInput) loginInput.disabled = loading;
|
|
if (passwordInput) passwordInput.disabled = loading;
|
|
if (rememberInput) rememberInput.disabled = loading;
|
|
|
|
if (buttonText && spinner) {
|
|
buttonText.style.display = loading ? "none" : "block";
|
|
spinner.style.display = loading ? "block" : "none";
|
|
}
|
|
}
|
|
|
|
function applyRememberToStorage(login, remember) {
|
|
if (remember) {
|
|
try {
|
|
localStorage.setItem(LS_LOGIN, login);
|
|
localStorage.setItem(LS_REMEMBER, "1");
|
|
} catch (e) {
|
|
/* ignore */
|
|
}
|
|
} else {
|
|
try {
|
|
localStorage.removeItem(LS_LOGIN);
|
|
localStorage.removeItem(LS_REMEMBER);
|
|
} catch (e) {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadRememberFromStorage() {
|
|
try {
|
|
if (localStorage.getItem(LS_REMEMBER) === "1") {
|
|
const saved = localStorage.getItem(LS_LOGIN);
|
|
if (saved) {
|
|
const loginInput = document.getElementById("loginInput");
|
|
const rememberInput = document.getElementById("rememberInput");
|
|
if (loginInput) loginInput.value = saved;
|
|
if (rememberInput) rememberInput.checked = true;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
async function checkSession() {
|
|
try {
|
|
const r = await fetch("/api/auth/check", { credentials: "same-origin" });
|
|
const data = await r.json();
|
|
if (data.authenticated && data.remember_login) {
|
|
const target = postLoginRedirectUrl();
|
|
await finishLoginTransition(target);
|
|
window.location.href = target;
|
|
}
|
|
} catch (e) {
|
|
/* stay on login */
|
|
}
|
|
}
|
|
|
|
function playLoginExitAnimation() {
|
|
return new Promise((resolve) => {
|
|
const page = document.querySelector(".z-login-page");
|
|
if (page) page.classList.add("z-login-page--hidden");
|
|
|
|
const backdrop = document.createElement("div");
|
|
backdrop.className = "z-login-backdrop";
|
|
document.body.appendChild(backdrop);
|
|
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "z-login-loader";
|
|
const root = document.createElement("div");
|
|
wrap.appendChild(root);
|
|
document.body.appendChild(wrap);
|
|
|
|
mountWespLogoLoader(root, {
|
|
loop: false,
|
|
durationMs: LOGIN_EXIT_ANIM_MS,
|
|
totalDurationMs: LOGIN_EXIT_ANIM_MS,
|
|
embedded: true,
|
|
transparentBackground: true,
|
|
knockoutBackground: true,
|
|
theme: themeForLogoLoader(),
|
|
onComplete: () => resolve(),
|
|
}).catch(() => resolve());
|
|
});
|
|
}
|
|
|
|
async function authenticate(event) {
|
|
event.preventDefault();
|
|
|
|
const login = document.getElementById("loginInput")?.value?.trim() || "";
|
|
const password = document.getElementById("passwordInput")?.value || "";
|
|
const remember = !!document.getElementById("rememberInput")?.checked;
|
|
|
|
if (!login) {
|
|
showError("Введите логин");
|
|
return;
|
|
}
|
|
if (!password) {
|
|
showError("Введите пароль");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const response = await fetch("/api/auth/login", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "same-origin",
|
|
body: JSON.stringify({ login, password, remember }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === "success") {
|
|
applyRememberToStorage(login, remember);
|
|
const target = postLoginRedirectUrl();
|
|
await finishLoginTransition(target);
|
|
window.location.href = target;
|
|
return;
|
|
}
|
|
|
|
showError(data.message || "Неверный логин или пароль");
|
|
const passwordInput = document.getElementById("passwordInput");
|
|
if (passwordInput) {
|
|
passwordInput.value = "";
|
|
passwordInput.focus();
|
|
}
|
|
setLoading(false);
|
|
} catch (error) {
|
|
showError("Ошибка соединения. Попробуйте снова.");
|
|
console.error("Authentication error:", error);
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
function initLoginPage() {
|
|
loadRememberFromStorage();
|
|
checkSession();
|
|
document.getElementById("loginInput")?.focus();
|
|
|
|
document.getElementById("loginForm")?.addEventListener("submit", authenticate);
|
|
document.getElementById("loginInput")?.addEventListener("keypress", (e) => {
|
|
if (e.key === "Enter") document.getElementById("passwordInput")?.focus();
|
|
});
|
|
document.getElementById("passwordInput")?.addEventListener("keypress", (e) => {
|
|
if (e.key === "Enter") authenticate(e);
|
|
});
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initLoginPage);
|
|
} else {
|
|
initLoginPage();
|
|
}
|