fix(iot-console): убрать namespace из UI полностью
- Поле Namespace удалено из формы входа
- namespace вычисляется из токена: SHA256(sub) → sless-{hex}
- navbar: убран badge с ns
- Телеметрия: убрана техническая подсказка про namespace
- Пользователь не видит и не вводит namespace нигде
This commit is contained in:
@@ -191,6 +191,7 @@
|
|||||||
const S = {
|
const S = {
|
||||||
// Сохраняемые настройки
|
// Сохраняемые настройки
|
||||||
token: localStorage.getItem('iot_token') || '',
|
token: localStorage.getItem('iot_token') || '',
|
||||||
|
// ns вычисляется из токена автоматически (SHA256(sub) → "sless-{hex}"), не вводится вручную
|
||||||
ns: localStorage.getItem('iot_ns') || '',
|
ns: localStorage.getItem('iot_ns') || '',
|
||||||
apiBase: localStorage.getItem('iot_api_base') || 'https://sless.kube5s.ru',
|
apiBase: localStorage.getItem('iot_api_base') || 'https://sless.kube5s.ru',
|
||||||
mqttBase: localStorage.getItem('iot_mqtt_base')|| 'ws://iot.kube5s.ru/mqtt',
|
mqttBase: localStorage.getItem('iot_mqtt_base')|| 'ws://iot.kube5s.ru/mqtt',
|
||||||
@@ -213,11 +214,39 @@ const S = {
|
|||||||
// Сохраняем auth-параметры в localStorage
|
// Сохраняем auth-параметры в localStorage
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
localStorage.setItem('iot_token', S.token);
|
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_api_base', S.apiBase);
|
||||||
localStorage.setItem('iot_mqtt_base',S.mqttBase);
|
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
|
// API
|
||||||
// =============================================================
|
// =============================================================
|
||||||
@@ -452,13 +481,10 @@ function renderLogin() {
|
|||||||
<label>MQTT Broker WebSocket</label>
|
<label>MQTT Broker WebSocket</label>
|
||||||
<input id="f-mqtt" value="${h(S.mqttBase)}" placeholder="ws://iot.kube5s.ru/mqtt">
|
<input id="f-mqtt" value="${h(S.mqttBase)}" placeholder="ws://iot.kube5s.ru/mqtt">
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label>Bearer токен</label>
|
<label>Bearer токен</label>
|
||||||
<input id="f-tok" type="password" value="${h(S.token)}" placeholder="eyJ...">
|
<input id="f-tok" type="password" value="${h(S.token)}" placeholder="eyJ...">
|
||||||
|
<div class="hint" style="margin-top:6px;">Данные вашего аккаунта определяются автоматически</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary mt8" style="width:100%;" id="login-btn" onclick="doLogin()">
|
<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() {
|
async function doLogin() {
|
||||||
const api = (document.getElementById('f-api').value || '').trim().replace(/\/$/, '');
|
const api = (document.getElementById('f-api').value || '').trim().replace(/\/$/, '');
|
||||||
const mqtt = (document.getElementById('f-mqtt').value || '').trim();
|
const mqtt = (document.getElementById('f-mqtt').value || '').trim();
|
||||||
const ns = (document.getElementById('f-ns').value || '').trim();
|
|
||||||
const tok = (document.getElementById('f-tok').value || '').trim();
|
const tok = (document.getElementById('f-tok').value || '').trim();
|
||||||
|
|
||||||
if (!ns) { loginErr('Укажите namespace'); return; }
|
|
||||||
if (!tok) { loginErr('Укажите токен'); 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.apiBase = api || 'https://sless.kube5s.ru';
|
||||||
S.mqttBase = mqtt || 'ws://iot.kube5s.ru/mqtt';
|
S.mqttBase = mqtt || 'ws://iot.kube5s.ru/mqtt';
|
||||||
S.ns = ns;
|
S.ns = ns;
|
||||||
S.token = tok;
|
S.token = tok;
|
||||||
|
|
||||||
const btn = document.getElementById('login-btn');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Проверка...';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiListDevices(); // проверяем что токен и namespace валидны
|
await apiListDevices(); // проверяем что токен валиден и API доступен
|
||||||
saveSettings();
|
saveSettings();
|
||||||
nav('devices');
|
nav('devices');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
S.token = '';
|
S.token = '';
|
||||||
|
S.ns = '';
|
||||||
loginErr(e.message);
|
loginErr(e.message);
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = 'Войти';
|
btn.textContent = 'Войти';
|
||||||
@@ -906,7 +943,7 @@ function telemetryTab() {
|
|||||||
<h3>Телеметрия</h3>
|
<h3>Телеметрия</h3>
|
||||||
<p class="hint" style="margin-top:8px;">Скоро — хранение данных и API чтения</p>
|
<p class="hint" style="margin-top:8px;">Скоро — хранение данных и API чтения</p>
|
||||||
<p class="hint" style="margin-top:4px;color:#334155;">
|
<p class="hint" style="margin-top:4px;color:#334155;">
|
||||||
Требует: Postgres StatefulSet в namespace <span class="mono">iot</span>
|
В разработке — появится в следующем обновлении
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -919,7 +956,6 @@ function navbar() {
|
|||||||
return `
|
return `
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<div class="navbar-brand" onclick="nav('devices')">⚡ IoT Консоль</div>
|
<div class="navbar-brand" onclick="nav('devices')">⚡ IoT Консоль</div>
|
||||||
<div class="navbar-ns">${h(S.ns)}</div>
|
|
||||||
<div class="navbar-spacer"></div>
|
<div class="navbar-spacer"></div>
|
||||||
<button class="btn btn-ghost btn-sm" onclick="doLogout()">Выйти</button>
|
<button class="btn btn-ghost btn-sm" onclick="doLogout()">Выйти</button>
|
||||||
</nav>`;
|
</nav>`;
|
||||||
|
|||||||
Reference in New Issue
Block a user