333 lines
9.8 KiB
JavaScript
333 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);
|