baton/frontend/admin.js
2026-03-20 23:44:58 +02:00

333 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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);