'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, '"'); } 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 = 'Нет пользователей'; 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 = ` ${u.id} ${_esc(u.name)} ${_esc(uuidShort)} ${u.is_blocked ? 'Заблокирован' : 'Активен'} ${_esc(date)} `; 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);