baton/frontend/admin.js

334 lines
9.8 KiB
JavaScript
Raw Permalink Normal View History

2026-03-20 23:44:58 +02:00
'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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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);