334 lines
9.8 KiB
JavaScript
334 lines
9.8 KiB
JavaScript
|
|
'use strict';
|
|||
|
|
|
|||
|
|
// ========== Token (sessionStorage — cleared on browser close) ==========
|
|||
|
|
|
|||
|
|
function _getToken() {
|
|||
|
|
return sessionStorage.getItem('baton_admin_token') || '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _saveToken(t) {
|
|||
|
|
sessionStorage.setItem('baton_admin_token', t);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _clearToken() {
|
|||
|
|
sessionStorage.removeItem('baton_admin_token');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== API wrapper ==========
|
|||
|
|
|
|||
|
|
async function _api(method, path, body) {
|
|||
|
|
const opts = {
|
|||
|
|
method,
|
|||
|
|
headers: { 'Authorization': 'Bearer ' + _getToken() },
|
|||
|
|
};
|
|||
|
|
if (body !== undefined) {
|
|||
|
|
opts.headers['Content-Type'] = 'application/json';
|
|||
|
|
opts.body = JSON.stringify(body);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const res = await fetch(path, opts);
|
|||
|
|
|
|||
|
|
if (res.status === 204) return null;
|
|||
|
|
|
|||
|
|
const text = await res.text().catch(() => '');
|
|||
|
|
if (!res.ok) {
|
|||
|
|
let detail = text;
|
|||
|
|
try { detail = JSON.parse(text).detail || text; } catch (_) {}
|
|||
|
|
throw new Error('HTTP ' + res.status + (detail ? ': ' + detail : ''));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try { return JSON.parse(text); } catch (_) { return null; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== UI helpers ==========
|
|||
|
|
|
|||
|
|
function _esc(str) {
|
|||
|
|
return String(str)
|
|||
|
|
.replace(/&/g, '&')
|
|||
|
|
.replace(/</g, '<')
|
|||
|
|
.replace(/>/g, '>')
|
|||
|
|
.replace(/"/g, '"');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _setError(id, msg) {
|
|||
|
|
const el = document.getElementById(id);
|
|||
|
|
el.textContent = msg;
|
|||
|
|
el.hidden = !msg;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _showPanel() {
|
|||
|
|
document.getElementById('screen-token').style.display = 'none';
|
|||
|
|
document.getElementById('screen-panel').classList.add('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _showTokenScreen() {
|
|||
|
|
document.getElementById('screen-panel').classList.remove('active');
|
|||
|
|
document.getElementById('screen-token').style.display = '';
|
|||
|
|
document.getElementById('token-input').value = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Users table ==========
|
|||
|
|
|
|||
|
|
function _renderTable(users) {
|
|||
|
|
const tbody = document.getElementById('users-tbody');
|
|||
|
|
tbody.innerHTML = '';
|
|||
|
|
|
|||
|
|
if (!users.length) {
|
|||
|
|
const tr = document.createElement('tr');
|
|||
|
|
tr.className = 'empty-row';
|
|||
|
|
tr.innerHTML = '<td colspan="6">Нет пользователей</td>';
|
|||
|
|
tbody.appendChild(tr);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
users.forEach((u) => {
|
|||
|
|
const tr = document.createElement('tr');
|
|||
|
|
if (u.is_blocked) tr.classList.add('is-blocked');
|
|||
|
|
|
|||
|
|
const date = u.created_at ? u.created_at.slice(0, 16).replace('T', ' ') : '—';
|
|||
|
|
const uuidShort = u.uuid ? u.uuid.slice(0, 8) + '…' : '—';
|
|||
|
|
|
|||
|
|
tr.innerHTML = `
|
|||
|
|
<td class="col-id">${u.id}</td>
|
|||
|
|
<td>${_esc(u.name)}</td>
|
|||
|
|
<td class="col-uuid" title="${_esc(u.uuid)}">${_esc(uuidShort)}</td>
|
|||
|
|
<td>
|
|||
|
|
<span class="badge ${u.is_blocked ? 'badge--blocked' : 'badge--active'}">
|
|||
|
|
${u.is_blocked ? 'Заблокирован' : 'Активен'}
|
|||
|
|
</span>
|
|||
|
|
</td>
|
|||
|
|
<td class="col-date">${_esc(date)}</td>
|
|||
|
|
<td class="col-actions">
|
|||
|
|
<button class="btn-sm"
|
|||
|
|
data-action="password"
|
|||
|
|
data-id="${u.id}"
|
|||
|
|
data-name="${_esc(u.name)}">Пароль</button>
|
|||
|
|
<button class="btn-sm ${u.is_blocked ? 'btn-sm--warn' : ''}"
|
|||
|
|
data-action="block"
|
|||
|
|
data-id="${u.id}"
|
|||
|
|
data-blocked="${u.is_blocked ? '1' : '0'}">
|
|||
|
|
${u.is_blocked ? 'Разблокировать' : 'Заблокировать'}
|
|||
|
|
</button>
|
|||
|
|
<button class="btn-sm btn-sm--danger"
|
|||
|
|
data-action="delete"
|
|||
|
|
data-id="${u.id}"
|
|||
|
|
data-name="${_esc(u.name)}">Удалить</button>
|
|||
|
|
</td>
|
|||
|
|
`;
|
|||
|
|
tbody.appendChild(tr);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Load users ==========
|
|||
|
|
|
|||
|
|
async function _loadUsers() {
|
|||
|
|
_setError('panel-error', '');
|
|||
|
|
try {
|
|||
|
|
const users = await _api('GET', '/admin/users');
|
|||
|
|
_renderTable(users);
|
|||
|
|
} catch (err) {
|
|||
|
|
_setError('panel-error', err.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Login / Logout ==========
|
|||
|
|
|
|||
|
|
async function _handleLogin() {
|
|||
|
|
const input = document.getElementById('token-input');
|
|||
|
|
const btn = document.getElementById('btn-login');
|
|||
|
|
const token = input.value.trim();
|
|||
|
|
if (!token) return;
|
|||
|
|
|
|||
|
|
btn.disabled = true;
|
|||
|
|
_setError('login-error', '');
|
|||
|
|
_saveToken(token);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const users = await _api('GET', '/admin/users');
|
|||
|
|
_renderTable(users);
|
|||
|
|
_showPanel();
|
|||
|
|
} catch (err) {
|
|||
|
|
_clearToken();
|
|||
|
|
const msg = err.message.includes('401') ? 'Неверный токен' : err.message;
|
|||
|
|
_setError('login-error', msg);
|
|||
|
|
btn.disabled = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _handleLogout() {
|
|||
|
|
_clearToken();
|
|||
|
|
_showTokenScreen();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Table action dispatcher (event delegation) ==========
|
|||
|
|
|
|||
|
|
async function _handleTableClick(e) {
|
|||
|
|
const btn = e.target.closest('[data-action]');
|
|||
|
|
if (!btn) return;
|
|||
|
|
|
|||
|
|
const { action, id, name, blocked } = btn.dataset;
|
|||
|
|
|
|||
|
|
if (action === 'password') {
|
|||
|
|
_openPasswordModal(id, name);
|
|||
|
|
} else if (action === 'block') {
|
|||
|
|
await _toggleBlock(id, blocked === '1');
|
|||
|
|
} else if (action === 'delete') {
|
|||
|
|
await _handleDelete(id, name);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Block / Unblock ==========
|
|||
|
|
|
|||
|
|
async function _toggleBlock(userId, currentlyBlocked) {
|
|||
|
|
_setError('panel-error', '');
|
|||
|
|
try {
|
|||
|
|
await _api('PUT', `/admin/users/${userId}/block`, { is_blocked: !currentlyBlocked });
|
|||
|
|
await _loadUsers();
|
|||
|
|
} catch (err) {
|
|||
|
|
_setError('panel-error', err.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Delete ==========
|
|||
|
|
|
|||
|
|
async function _handleDelete(userId, userName) {
|
|||
|
|
if (!confirm(`Удалить пользователя "${userName}"?\n\nБудут удалены все его сигналы. Действие нельзя отменить.`)) return;
|
|||
|
|
_setError('panel-error', '');
|
|||
|
|
try {
|
|||
|
|
await _api('DELETE', `/admin/users/${userId}`);
|
|||
|
|
await _loadUsers();
|
|||
|
|
} catch (err) {
|
|||
|
|
_setError('panel-error', err.message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Password modal ==========
|
|||
|
|
|
|||
|
|
function _openPasswordModal(userId, userName) {
|
|||
|
|
document.getElementById('modal-pw-subtitle').textContent = `Пользователь: ${userName}`;
|
|||
|
|
document.getElementById('modal-pw-user-id').value = userId;
|
|||
|
|
document.getElementById('new-password').value = '';
|
|||
|
|
_setError('modal-pw-error', '');
|
|||
|
|
document.getElementById('btn-pw-save').disabled = false;
|
|||
|
|
document.getElementById('modal-password').hidden = false;
|
|||
|
|
document.getElementById('new-password').focus();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _closePasswordModal() {
|
|||
|
|
document.getElementById('modal-password').hidden = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function _handleSetPassword() {
|
|||
|
|
const userId = document.getElementById('modal-pw-user-id').value;
|
|||
|
|
const password = document.getElementById('new-password').value;
|
|||
|
|
const btn = document.getElementById('btn-pw-save');
|
|||
|
|
|
|||
|
|
if (!password) {
|
|||
|
|
_setError('modal-pw-error', 'Введите пароль');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
btn.disabled = true;
|
|||
|
|
_setError('modal-pw-error', '');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await _api('PUT', `/admin/users/${userId}/password`, { password });
|
|||
|
|
_closePasswordModal();
|
|||
|
|
} catch (err) {
|
|||
|
|
_setError('modal-pw-error', err.message);
|
|||
|
|
btn.disabled = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Create user modal ==========
|
|||
|
|
|
|||
|
|
function _openCreateModal() {
|
|||
|
|
document.getElementById('create-uuid').value = crypto.randomUUID();
|
|||
|
|
document.getElementById('create-name').value = '';
|
|||
|
|
document.getElementById('create-password').value = '';
|
|||
|
|
_setError('create-error', '');
|
|||
|
|
document.getElementById('btn-create-submit').disabled = false;
|
|||
|
|
document.getElementById('modal-create').hidden = false;
|
|||
|
|
document.getElementById('create-name').focus();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _closeCreateModal() {
|
|||
|
|
document.getElementById('modal-create').hidden = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function _handleCreateUser() {
|
|||
|
|
const uuid = document.getElementById('create-uuid').value.trim();
|
|||
|
|
const name = document.getElementById('create-name').value.trim();
|
|||
|
|
const password = document.getElementById('create-password').value;
|
|||
|
|
const btn = document.getElementById('btn-create-submit');
|
|||
|
|
|
|||
|
|
if (!uuid || !name) {
|
|||
|
|
_setError('create-error', 'UUID и имя обязательны');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
btn.disabled = true;
|
|||
|
|
_setError('create-error', '');
|
|||
|
|
|
|||
|
|
const body = { uuid, name };
|
|||
|
|
if (password) body.password = password;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await _api('POST', '/admin/users', body);
|
|||
|
|
_closeCreateModal();
|
|||
|
|
await _loadUsers();
|
|||
|
|
} catch (err) {
|
|||
|
|
const msg = err.message.includes('409') ? 'Пользователь с таким UUID уже существует' : err.message;
|
|||
|
|
_setError('create-error', msg);
|
|||
|
|
btn.disabled = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Init ==========
|
|||
|
|
|
|||
|
|
function _init() {
|
|||
|
|
// Login screen
|
|||
|
|
document.getElementById('btn-login').addEventListener('click', _handleLogin);
|
|||
|
|
document.getElementById('token-input').addEventListener('keydown', (e) => {
|
|||
|
|
if (e.key === 'Enter') _handleLogin();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Panel
|
|||
|
|
document.getElementById('btn-logout').addEventListener('click', _handleLogout);
|
|||
|
|
document.getElementById('btn-create').addEventListener('click', _openCreateModal);
|
|||
|
|
|
|||
|
|
// Table (event delegation)
|
|||
|
|
document.getElementById('users-table').addEventListener('click', _handleTableClick);
|
|||
|
|
|
|||
|
|
// Password modal
|
|||
|
|
document.getElementById('btn-pw-cancel').addEventListener('click', _closePasswordModal);
|
|||
|
|
document.getElementById('btn-pw-save').addEventListener('click', _handleSetPassword);
|
|||
|
|
document.getElementById('new-password').addEventListener('keydown', (e) => {
|
|||
|
|
if (e.key === 'Enter') _handleSetPassword();
|
|||
|
|
});
|
|||
|
|
document.getElementById('modal-password').addEventListener('click', (e) => {
|
|||
|
|
if (e.target.id === 'modal-password') _closePasswordModal();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Create modal
|
|||
|
|
document.getElementById('btn-create-cancel').addEventListener('click', _closeCreateModal);
|
|||
|
|
document.getElementById('btn-create-submit').addEventListener('click', _handleCreateUser);
|
|||
|
|
document.getElementById('create-password').addEventListener('keydown', (e) => {
|
|||
|
|
if (e.key === 'Enter') _handleCreateUser();
|
|||
|
|
});
|
|||
|
|
document.getElementById('modal-create').addEventListener('click', (e) => {
|
|||
|
|
if (e.target.id === 'modal-create') _closeCreateModal();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Auto-login if token is already saved in sessionStorage
|
|||
|
|
if (_getToken()) {
|
|||
|
|
_showPanel();
|
|||
|
|
_loadUsers().catch(() => {
|
|||
|
|
_clearToken();
|
|||
|
|
_showTokenScreen();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.addEventListener('DOMContentLoaded', _init);
|