v0.1.16: JWT auth via nubes API, auto-provisioning, UI login

- app/auth/jwt.go: ParseJWTClaims, TenantIDFromSub (sless-compatible SHA256), PingNubesAPI
- app/admin/admin.go: POST /ui/api/auth endpoint, jwtMiddleware for /ui/api/*
- app/tenant/tenant_store.go: NubesSub/Email fields, GetBySub, CreateFromJWT
- app/ui/index.html: login page, email in navbar, JWT session in localStorage
- deployments/k8s/deployment.yaml: v0.1.16, NUBES_ENDPOINT env
- doc/decisions/resource-limits-plan.md: 20 vulnerabilities audit
- Fix: /ui/api/auth moved to subrouter (gorilla/mux PathPrefix conflict)
This commit is contained in:
Naeel
2026-04-10 18:35:19 +03:00
parent 6fb160f8ae
commit b9d434bcc5
9 changed files with 854 additions and 25 deletions
+121 -12
View File
@@ -3,9 +3,9 @@
app/ui/index.html
SQS Console — веб-интерфейс для shared-sqs (Nubes branding)
Created: 2026-04-10
Updated: 2026-04-10 — queue CRUD (create/delete) + message peek/send/purge
Updated: 2026-04-10 — JWT auth через nubes token, email в navbar, auto-provisioning
Vanilla HTML/CSS/JS SPA. Встраивается через go:embed.
Режим: Публичный UI API без авторизации (демо). Все данные in-memory.
Режим: JWT авторизация через nubes API. Пользователь вводит токен → валидация → сессия.
-->
<html lang="ru">
<head>
@@ -324,15 +324,33 @@ td.msg-expand { padding: 0 !important; border-bottom: 1px solid var(--border); }
</head>
<body>
<!-- ===== APP SHELL (всегда видим, без логина) ===== -->
<div id="app">
<!-- ===== LOGIN PAGE ===== -->
<div id="login-page" class="login-page">
<div class="login-box">
<img src="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/logo.svg" alt="Nubes">
<h1>SQS CONSOLE</h1>
<div class="form-group">
<label for="login-token">API Token (nubes JWT)</label>
<input id="login-token" type="password" placeholder="eyJhbGciOiJSUzI1NiIs...">
</div>
<div id="login-error" class="login-error"></div>
<button class="btn btn-primary" style="width:100%" onclick="doLogin()">Войти</button>
<p style="font-size:12px;color:var(--text-secondary);margin-top:16px">
Токен можно получить в панели управления облаком Nubes
</p>
</div>
</div>
<!-- ===== APP SHELL (показывается после авторизации) ===== -->
<div id="app" class="hidden">
<nav class="navbar">
<div class="navbar-brand">
<img src="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/logo.svg" alt="Nubes">
<span>SQS CONSOLE</span>
</div>
<div class="navbar-user">
<span>admin</span>
<span id="user-email" style="color:var(--accent)"></span>
<button class="btn-logout" onclick="doLogout()">Выйти</button>
</div>
</nav>
<div class="container">
@@ -447,23 +465,104 @@ let msgCache = {}; // id → объект сообщения для d
let _sendState = { tenantId: null, queueName: null }; // контекст sendMessage
let _createQueueState = { tenantId: null }; // контекст createQueue
// Session state — JWT auth
let sessionToken = null; // JWT токен для Authorization header
let sessionEmail = null; // Email из JWT для отображения в UI
// ===== INIT =====
// Запуск — сразу показываем dashboard без логина
// Проверяем сохранённый токен в localStorage при загрузке
(function init() {
BASE = window.location.origin;
showDashboard();
const saved = localStorage.getItem('sqs_session');
if (saved) {
try {
const s = JSON.parse(saved);
if (s.token && s.email) {
sessionToken = s.token;
sessionEmail = s.email;
enterApp();
return;
}
} catch(e) { /* corrupted — show login */ }
}
showLogin();
})();
// ===== LOGIN / LOGOUT =====
// showLogin — показывает страницу ввода токена
function showLogin() {
document.getElementById('login-page').classList.remove('hidden');
document.getElementById('app').classList.add('hidden');
if (refreshTimer) clearInterval(refreshTimer);
}
// doLogin — отправляет токен на /ui/api/auth, при успехе входит в app
function doLogin() {
const token = document.getElementById('login-token').value.trim();
if (!token) {
document.getElementById('login-error').textContent = 'Введите токен';
return;
}
document.getElementById('login-error').textContent = 'Проверка токена...';
fetch(BASE + '/ui/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token })
})
.then(r => {
if (!r.ok) return r.json().then(d => { throw new Error(d.error || r.statusText); });
return r.json();
})
.then(data => {
sessionToken = data.token;
sessionEmail = data.email || data.tenant_id;
localStorage.setItem('sqs_session', JSON.stringify({
token: sessionToken,
email: sessionEmail
}));
enterApp();
})
.catch(err => {
document.getElementById('login-error').textContent = 'Ошибка: ' + err.message;
});
}
// doLogout — очищает сессию, показывает login
function doLogout() {
sessionToken = null;
sessionEmail = null;
localStorage.removeItem('sqs_session');
if (refreshTimer) clearInterval(refreshTimer);
showLogin();
}
// enterApp — переключает UI с login на app shell
function enterApp() {
document.getElementById('login-page').classList.add('hidden');
document.getElementById('app').classList.remove('hidden');
document.getElementById('user-email').textContent = sessionEmail || '';
showDashboard();
}
// ===== API HELPER =====
// api — выполняет запрос к публичному UI API (без auth)
// api — выполняет запрос к UI API с JWT Authorization header
function api(path, opts = {}) {
const headers = {
'Content-Type': 'application/json',
...(opts.headers || {})
};
if (sessionToken) {
headers['Authorization'] = 'Bearer ' + sessionToken;
}
return fetch(BASE + '/ui/api' + path, {
...opts,
headers: {
'Content-Type': 'application/json',
...(opts.headers || {})
}
headers: headers
}).then(r => {
if (r.status === 401 || r.status === 403) {
// Токен истёк или невалиден — выбросить на login
doLogout();
throw new Error('Session expired — please login again');
}
if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
if (r.status === 204) return null;
return r.json();
@@ -927,6 +1026,16 @@ function purgeQueueConfirm(tenantId, queueName) {
})
.catch(err => alert('Ошибка: ' + err.message));
}
// Клавиша Enter на поле токена = нажатие "Войти"
document.addEventListener('DOMContentLoaded', function() {
const tokenInput = document.getElementById('login-token');
if (tokenInput) {
tokenInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') doLogin();
});
}
});
</script>
</body>
</html>