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>
215 lines
8.0 KiB
JavaScript
215 lines
8.0 KiB
JavaScript
'use strict';
|
||
|
||
let canManage = false;
|
||
|
||
function escHtml(s) {
|
||
return String(s)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
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();
|
||
});
|