diff --git a/crypto_spot_bot/web/dashboard.js b/crypto_spot_bot/web/dashboard.js index 71d8ac3..40a562b 100644 --- a/crypto_spot_bot/web/dashboard.js +++ b/crypto_spot_bot/web/dashboard.js @@ -2,7 +2,7 @@ const state = { snapshot: null, - authorization: "", + token: "", loading: false, timer: null, marketFilter: "", @@ -78,30 +78,49 @@ function bindControls() { event.preventDefault(); const username = $("#usernameInput").value.trim(); const password = $("#passwordInput").value; + const submitButton = $("#authSubmitButton"); setText("authError", ""); if (!username || !password) return; - state.authorization = basicAuthorization(username, password); - await loadSnapshot(true, true); + if (username !== "sevenhill") { + setText("authError", "Неверный логин или пароль."); + return; + } + state.token = password.trim(); + submitButton.disabled = true; + submitButton.setAttribute("aria-busy", "true"); + setText("authSubmitButton", "Входим…"); + setText("authError", "Проверяем доступ…"); + try { + await loadSnapshot(true, true); + } finally { + submitButton.disabled = false; + submitButton.removeAttribute("aria-busy"); + setText("authSubmitButton", "Войти"); + } }); } -function basicAuthorization(username, password) { - const bytes = new TextEncoder().encode(`${username}:${password}`); - let binary = ""; - bytes.forEach((byte) => { binary += String.fromCharCode(byte); }); - return `Basic ${btoa(binary)}`; -} - async function api(path, options = {}) { const headers = { Accept: "application/json", ...(options.headers || {}) }; if (options.body) headers["Content-Type"] = "application/json"; - if (state.authorization) headers.Authorization = state.authorization; - const response = await fetch(path, { - ...options, - headers, - credentials: "same-origin", - cache: "no-store", - }); + if (state.token) headers["X-TradeBot-Token"] = state.token; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30000); + let response; + try { + response = await fetch(path, { + ...options, + headers, + signal: controller.signal, + credentials: "omit", + cache: "no-store", + }); + } catch (error) { + if (error?.name === "AbortError") throw new Error("Сервер не ответил за 30 секунд."); + throw error; + } finally { + clearTimeout(timeout); + } if (response.status === 401) throw new AuthRequiredError("Требуется авторизация"); let payload = null; try { payload = await response.json(); } catch (_) { payload = null; } @@ -113,7 +132,7 @@ async function api(path, options = {}) { } async function loadSnapshot(manual = false, fromAuth = false) { - if (state.loading) return; + if (state.loading) return false; state.loading = true; clearTimeout(state.timer); $("#refreshButton").classList.add("is-spinning"); @@ -127,15 +146,20 @@ async function loadSnapshot(manual = false, fromAuth = false) { $("#passwordInput").value = ""; setText("authError", ""); scheduleRefresh(10000); + return true; } catch (error) { if (error instanceof AuthRequiredError) { if (fromAuth) setText("authError", "Неверный логин или пароль."); - state.authorization = ""; + state.token = ""; + showAuthDialog(); + } else if (fromAuth) { + setText("authError", `Ошибка подключения: ${error.message}`); showAuthDialog(); } else { setOffline(true, error.message); scheduleRefresh(12000); } + return false; } finally { state.loading = false; $("#refreshButton").classList.remove("is-spinning"); diff --git a/crypto_spot_bot/web/index.html b/crypto_spot_bot/web/index.html index a1b1793..508016e 100644 --- a/crypto_spot_bot/web/index.html +++ b/crypto_spot_bot/web/index.html @@ -195,7 +195,7 @@
Защищённый доступ
Введите логин и пароль панели управления. Данные используются только для запросов из этой вкладки и не сохраняются в браузере.
+Введите логин и пароль панели управления TradeBot. Данные используются только для запросов из этой вкладки и не сохраняются в браузере.