From 017312f35c0f702070b56b947946153fcf8f4911 Mon Sep 17 00:00:00 2001 From: Naeel Date: Sat, 4 Apr 2026 19:38:32 +0300 Subject: [PATCH] =?UTF-8?q?fix(iot-console):=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20namespace=20=D0=B8=D0=B7=20UI=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D0=BE=D1=81=D1=82=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Поле Namespace удалено из формы входа - namespace вычисляется из токена: SHA256(sub) → sless-{hex} - navbar: убран badge с ns - Телеметрия: убрана техническая подсказка про namespace - Пользователь не видит и не вводит namespace нигде --- internal/api/ui/iot-console.html | 66 ++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/internal/api/ui/iot-console.html b/internal/api/ui/iot-console.html index dc42157..217624d 100644 --- a/internal/api/ui/iot-console.html +++ b/internal/api/ui/iot-console.html @@ -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() { -
- - -
+
Данные вашего аккаунта определяются автоматически
`;