fix(iot-console): убрать namespace из UI полностью

- Поле Namespace удалено из формы входа
- namespace вычисляется из токена: SHA256(sub) → sless-{hex}
- navbar: убран badge с ns
- Телеметрия: убрана техническая подсказка про namespace
- Пользователь не видит и не вводит namespace нигде
This commit is contained in:
Naeel
2026-04-04 19:38:32 +03:00
parent b48c300ac5
commit 017312f35c
+51 -15
View File
@@ -191,6 +191,7 @@
const S = {
// Сохраняемые настройки
token: localStorage.getItem('iot_token') || '',
// ns вычисляется из токена автоматически (SHA256(sub) → "sless-{hex}"), не вводится вручную
ns: localStorage.getItem('iot_ns') || '',
apiBase: localStorage.getItem('iot_api_base') || 'https://sless.kube5s.ru',
mqttBase: localStorage.getItem('iot_mqtt_base')|| 'ws://iot.kube5s.ru/mqtt',
@@ -213,11 +214,39 @@ const S = {
// Сохраняем auth-параметры в localStorage
function saveSettings() {
localStorage.setItem('iot_token', S.token);
localStorage.setItem('iot_ns', S.ns);
localStorage.setItem('iot_ns', S.ns); // сохраняем для восстановления сессии
localStorage.setItem('iot_api_base', S.apiBase);
localStorage.setItem('iot_mqtt_base',S.mqttBase);
}
// namespaceFromToken — вычисляет k8s namespace из JWT токена.
// Алгоритм ИДЕНТИЧЕН Terraform провайдеру (client.NamespaceFromSub):
// JWT payload → sub claim → SHA256(sub) → первые 8 байт → "sless-{16 hex символов}"
// Пользователь НИКОГДА не вводит namespace вручную — это гарантирует изоляцию.
async function namespaceFromToken(token) {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Неверный формат токена (не JWT)');
// base64url → base64 (заменяем символы, добавляем padding)
let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
while (payload.length % 4) payload += '=';
let claims;
try { claims = JSON.parse(atob(payload)); }
catch(e) { throw new Error('Не удалось декодировать токен: ' + e.message); }
if (!claims.sub) throw new Error('В токене нет поля sub — токен не поддерживается');
// SHA256(sub) через Web Crypto API (встроен в все современные браузеры)
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(claims.sub));
// Берём первые 8 байт → 16 hex символов — идентично Go: hash[:8]
const hex = Array.from(new Uint8Array(buf).slice(0, 8))
.map(b => b.toString(16).padStart(2, '0')).join('');
return 'sless-' + hex;
}
// =============================================================
// API
// =============================================================
@@ -452,13 +481,10 @@ function renderLogin() {
<label>MQTT Broker WebSocket</label>
<input id="f-mqtt" value="${h(S.mqttBase)}" placeholder="ws://iot.kube5s.ru/mqtt">
</div>
<div class="form-group">
<label>Namespace</label>
<input id="f-ns" value="${h(S.ns)}" placeholder="my-namespace" autocomplete="off">
</div>
<div class="form-group">
<label>Bearer токен</label>
<input id="f-tok" type="password" value="${h(S.token)}" placeholder="eyJ...">
<div class="hint" style="margin-top:6px;">Данные вашего аккаунта определяются автоматически</div>
</div>
<button class="btn btn-primary mt8" style="width:100%;" id="login-btn" onclick="doLogin()">
Войти
@@ -471,31 +497,42 @@ function renderLogin() {
});
}
// doLogin — проверить настройки и перейти к устройствам
// doLogin — вычислить namespace из токена, проверить доступность API, перейти к устройствам.
// Пользователь НИКОГДА не вводит namespace — он однозначно определяется из токена.
async function doLogin() {
const api = (document.getElementById('f-api').value || '').trim().replace(/\/$/, '');
const mqtt = (document.getElementById('f-mqtt').value || '').trim();
const ns = (document.getElementById('f-ns').value || '').trim();
const tok = (document.getElementById('f-tok').value || '').trim();
if (!ns) { loginErr('Укажите namespace'); return; }
if (!tok) { loginErr('Укажите токен'); return; }
const btn = document.getElementById('login-btn');
btn.disabled = true;
btn.textContent = 'Проверка...';
// Вычисляем namespace из токена — SHA256(sub) → "sless-{hex}"
let ns;
try {
ns = await namespaceFromToken(tok);
} catch(e) {
loginErr('Ошибка токена: ' + e.message);
btn.disabled = false;
btn.textContent = 'Войти';
return;
}
S.apiBase = api || 'https://sless.kube5s.ru';
S.mqttBase = mqtt || 'ws://iot.kube5s.ru/mqtt';
S.ns = ns;
S.token = tok;
const btn = document.getElementById('login-btn');
btn.disabled = true;
btn.textContent = 'Проверка...';
try {
await apiListDevices(); // проверяем что токен и namespace валидны
await apiListDevices(); // проверяем что токен валиден и API доступен
saveSettings();
nav('devices');
} catch(e) {
S.token = '';
S.ns = '';
loginErr(e.message);
btn.disabled = false;
btn.textContent = 'Войти';
@@ -906,7 +943,7 @@ function telemetryTab() {
<h3>Телеметрия</h3>
<p class="hint" style="margin-top:8px;">Скоро — хранение данных и API чтения</p>
<p class="hint" style="margin-top:4px;color:#334155;">
Требует: Postgres StatefulSet в namespace <span class="mono">iot</span>
В разработке — появится в следующем обновлении
</p>
</div>
</div>`;
@@ -919,7 +956,6 @@ function navbar() {
return `
<nav class="navbar">
<div class="navbar-brand" onclick="nav('devices')">⚡ IoT Консоль</div>
<div class="navbar-ns">${h(S.ns)}</div>
<div class="navbar-spacer"></div>
<button class="btn btn-ghost btn-sm" onclick="doLogout()">Выйти</button>
</nav>`;