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(); }