diff --git a/crypto_spot_bot/web/dashboard.css b/crypto_spot_bot/web/dashboard.css index 2c1e8d2..7623511 100644 --- a/crypto_spot_bot/web/dashboard.css +++ b/crypto_spot_bot/web/dashboard.css @@ -338,6 +338,7 @@ tbody tr:hover td { background: rgba(255,255,255,.012); } .dialog p:not(.eyebrow):not(.form-error) { margin: 11px 0 22px; color: var(--muted); font-size: 11px; line-height: 1.6; } .dialog label { display: block; margin-bottom: 8px; color: var(--muted); font-size: 10px; } .dialog input { width: 100%; height: 40px; padding: 0 11px; border: 1px solid var(--line); border-radius: 7px; outline: 0; background: #090c10; color: var(--text); } +.auth-fields { display: grid; gap: 14px; } .dialog .button-wide { margin-top: 14px; } .form-error { min-height: 16px; margin: 8px 0 0; color: var(--red); font-size: 9px; } .dialog-actions { display: flex; justify-content: flex-end; gap: 9px; } diff --git a/crypto_spot_bot/web/dashboard.js b/crypto_spot_bot/web/dashboard.js index e990789..71d8ac3 100644 --- a/crypto_spot_bot/web/dashboard.js +++ b/crypto_spot_bot/web/dashboard.js @@ -2,7 +2,7 @@ const state = { snapshot: null, - token: "", + authorization: "", loading: false, timer: null, marketFilter: "", @@ -32,7 +32,7 @@ document.addEventListener("DOMContentLoaded", () => { bindNavigation(); bindControls(); selectPage(location.hash.slice(1) || "overview", false); - loadSnapshot(); + showAuthDialog(); }); function bindNavigation() { @@ -76,17 +76,26 @@ function bindControls() { }); $("#authForm").addEventListener("submit", async (event) => { event.preventDefault(); - state.token = $("#tokenInput").value.trim(); + const username = $("#usernameInput").value.trim(); + const password = $("#passwordInput").value; setText("authError", ""); - if (!state.token) return; + if (!username || !password) return; + state.authorization = basicAuthorization(username, password); await loadSnapshot(true, true); }); } +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.token) headers["X-TradeBot-Token"] = state.token; + if (state.authorization) headers.Authorization = state.authorization; const response = await fetch(path, { ...options, headers, @@ -115,11 +124,13 @@ async function loadSnapshot(manual = false, fromAuth = false) { render(snapshot); setOffline(false); if ($("#authDialog").open) $("#authDialog").close(); + $("#passwordInput").value = ""; setText("authError", ""); scheduleRefresh(10000); } catch (error) { if (error instanceof AuthRequiredError) { - if (fromAuth) setText("authError", "Токен не принят сервером."); + if (fromAuth) setText("authError", "Неверный логин или пароль."); + state.authorization = ""; showAuthDialog(); } else { setOffline(true, error.message); @@ -140,7 +151,7 @@ function showAuthDialog() { const dialog = $("#authDialog"); if (!dialog.open) dialog.showModal(); setText("syncLabel", "Нужна авторизация"); - setTimeout(() => $("#tokenInput").focus(), 50); + setTimeout(() => $("#usernameInput").focus(), 50); } function setOffline(offline, message = "") { diff --git a/crypto_spot_bot/web/index.html b/crypto_spot_bot/web/index.html index d8068ee..a1b1793 100644 --- a/crypto_spot_bot/web/index.html +++ b/crypto_spot_bot/web/index.html @@ -194,12 +194,20 @@
T

Защищённый доступ

-

Требуется API-токен

-

Прокси-авторизация не обнаружена. Токен останется только в памяти этой вкладки и не будет сохранён.

- - +

Вход в TradeBot

+

Введите логин и пароль панели управления. Данные используются только для запросов из этой вкладки и не сохраняются в браузере.

+
+
+ + +
+
+ + +
+
- +
@@ -213,6 +221,6 @@
- + diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index e8a3bd3..c36c502 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -64,13 +64,16 @@ def test_safe_config_summarizes_torch_forecast_artifact(make_settings, tmp_path) def test_web_ui_assets_are_available() -> None: html = WEB_INDEX.read_text(encoding="utf-8") + script = WEB_INDEX.with_name("dashboard.js").read_text(encoding="utf-8") assert "TradeBot — панель управления" in html assert "/assets/dashboard.css" in html assert "/assets/dashboard.js" in html - assert "/web-api/dashboard/snapshot" in WEB_INDEX.with_name("dashboard.js").read_text( - encoding="utf-8" - ) + assert 'id="usernameInput"' in html + assert 'id="passwordInput"' in html + assert "/web-api/dashboard/snapshot" in script + assert "headers.Authorization = state.authorization" in script + assert "X-TradeBot-Token" not in script def test_compact_markets_keeps_dashboard_fields_and_limits_candles() -> None: