Files
PriceForge/web/static/js/db_users.js
Mikhail Chusavitin 47b260dad4 Add QFS DB user management page
Create/edit/delete MariaDB users for the QFS child application via
the PriceForge admin UI. On creation the full standard QFS grant set
(14 table-level GRANTs) is applied automatically. Page shows a
privilege warning when the PriceForge DB user lacks CREATE USER rights.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 18:14:07 +03:00

215 lines
8.0 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';
let canManage = false;
function escHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
async function checkCanManage() {
try {
const resp = await fetch('/api/db-users/can-manage');
const data = await resp.json();
canManage = data.can_manage === true;
const banner = document.getElementById('no-privileges-banner');
const addBtn = document.getElementById('add-user-btn');
if (canManage) {
banner.classList.add('hidden');
addBtn.classList.remove('hidden');
} else {
banner.classList.remove('hidden');
addBtn.classList.add('hidden');
const reasonEl = document.getElementById('no-privileges-reason');
if (data.reason) reasonEl.textContent = ' ' + data.reason + ' ';
}
} catch (e) {
console.error('can-manage check failed:', e);
}
}
async function loadUsers() {
const tbody = document.getElementById('users-body');
try {
const resp = await fetch('/api/db-users');
if (!resp.ok) {
const err = await resp.json().catch(() => ({ error: resp.statusText }));
tbody.innerHTML = `<tr><td colspan="4" class="px-4 py-6 text-center text-red-500">${escHtml(err.error || 'Ошибка загрузки')}</td></tr>`;
return;
}
const data = await resp.json();
const users = data.users || [];
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="px-4 py-6 text-center text-gray-400">Пользователи не найдены</td></tr>';
return;
}
tbody.innerHTML = users.map(u => `
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-mono text-sm">${escHtml(u.username)}</td>
<td class="px-4 py-3 text-sm text-gray-600">${escHtml(u.host)}</td>
<td class="px-4 py-3 text-sm">
${u.has_password
? '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-green-100 text-green-700">Задан</span>'
: '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-yellow-100 text-yellow-700">Нет</span>'
}
</td>
<td class="px-4 py-3 text-right space-x-2">
${canManage ? `
<button onclick="openEditModal('${escHtml(u.username)}','${escHtml(u.host)}')"
class="text-xs px-3 py-1 border rounded hover:bg-gray-50 text-gray-700">
Сменить пароль
</button>
<button onclick="deleteUser('${escHtml(u.username)}','${escHtml(u.host)}')"
class="text-xs px-3 py-1 border rounded hover:bg-red-50 text-red-600 border-red-200">
Удалить
</button>
` : ''}
</td>
</tr>
`).join('');
} catch (e) {
tbody.innerHTML = `<tr><td colspan="4" class="px-4 py-6 text-center text-red-500">${escHtml(String(e))}</td></tr>`;
}
}
// --- Create modal ---
function openCreateModal() {
document.getElementById('create-form').reset();
document.getElementById('create-host').value = '%';
document.getElementById('create-error').classList.add('hidden');
const modal = document.getElementById('create-modal');
modal.classList.remove('hidden');
modal.classList.add('flex');
document.getElementById('create-username').focus();
}
function closeCreateModal() {
const modal = document.getElementById('create-modal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
async function createUser(event) {
event.preventDefault();
const btn = document.getElementById('create-save-btn');
const errEl = document.getElementById('create-error');
errEl.classList.add('hidden');
const username = document.getElementById('create-username').value.trim();
const host = document.getElementById('create-host').value.trim() || '%';
const password = document.getElementById('create-password').value;
btn.disabled = true;
btn.textContent = 'Создание...';
try {
const resp = await fetch('/api/db-users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, host, password }),
});
const data = await resp.json();
if (!resp.ok) {
errEl.textContent = data.error || 'Ошибка';
errEl.classList.remove('hidden');
return;
}
closeCreateModal();
showToast(`Пользователь ${username}@${host} создан`, 'success');
await loadUsers();
} catch (e) {
errEl.textContent = String(e);
errEl.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = 'Создать';
}
}
// --- Edit password modal ---
function openEditModal(username, host) {
document.getElementById('edit-username').value = username;
document.getElementById('edit-host').value = host;
document.getElementById('edit-user-label').textContent = `${username}@${host}`;
document.getElementById('edit-password').value = '';
document.getElementById('edit-error').classList.add('hidden');
const modal = document.getElementById('edit-modal');
modal.classList.remove('hidden');
modal.classList.add('flex');
document.getElementById('edit-password').focus();
}
function closeEditModal() {
const modal = document.getElementById('edit-modal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
async function updatePassword(event) {
event.preventDefault();
const btn = document.getElementById('edit-save-btn');
const errEl = document.getElementById('edit-error');
errEl.classList.add('hidden');
const username = document.getElementById('edit-username').value;
const host = document.getElementById('edit-host').value;
const password = document.getElementById('edit-password').value;
btn.disabled = true;
btn.textContent = 'Сохранение...';
try {
const resp = await fetch(`/api/db-users/${encodeURIComponent(username)}?host=${encodeURIComponent(host)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
const data = await resp.json();
if (!resp.ok) {
errEl.textContent = data.error || 'Ошибка';
errEl.classList.remove('hidden');
return;
}
closeEditModal();
showToast(`Пароль для ${username}@${host} изменён`, 'success');
} catch (e) {
errEl.textContent = String(e);
errEl.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = 'Сохранить';
}
}
// --- Delete ---
async function deleteUser(username, host) {
if (!confirm(`Удалить пользователя ${username}@${host}?\n\nВсе его права будут отозваны. Это действие необратимо.`)) {
return;
}
try {
const resp = await fetch(`/api/db-users/${encodeURIComponent(username)}?host=${encodeURIComponent(host)}`, {
method: 'DELETE',
});
const data = await resp.json();
if (!resp.ok) {
showToast(data.error || 'Ошибка удаления', 'error');
return;
}
showToast(`Пользователь ${username}@${host} удалён`, 'success');
await loadUsers();
} catch (e) {
showToast(String(e), 'error');
}
}
document.addEventListener('DOMContentLoaded', async function () {
await checkCanManage();
await loadUsers();
});