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:
+121
-12
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user