nextcloud-install/admin-ui/public/app.js
2026-06-24 09:23:58 +03:00

283 lines
9.0 KiB
JavaScript

const $ = (sel) => document.querySelector(sel);
const loginScreen = $('#login-screen');
const mainScreen = $('#main-screen');
const loginForm = $('#login-form');
const loginError = $('#login-error');
const usersBody = $('#users-body');
const userDialog = $('#user-dialog');
const userForm = $('#user-form');
const globalError = $('#global-error');
const globalOk = $('#global-ok');
let allGroups = [];
let allUsers = [];
async function api(path, opts = {}) {
const res = await fetch(path, {
headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) },
...opts,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
return data;
}
function showError(msg) {
globalError.textContent = msg;
globalError.classList.remove('hidden');
globalOk.classList.add('hidden');
setTimeout(() => globalError.classList.add('hidden'), 6000);
}
function showOk(msg) {
globalOk.textContent = msg;
globalOk.classList.remove('hidden');
globalError.classList.add('hidden');
setTimeout(() => globalOk.classList.add('hidden'), 4000);
}
function formatBytes(n) {
if (n == null || n === '?' || isNaN(Number(n))) return '—';
const b = Number(n);
if (b === 0) return '0 B';
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.min(Math.floor(Math.log(b) / Math.log(1024)), u.length - 1);
return `${(b / 1024 ** i).toFixed(1)} ${u[i]}`;
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
async function checkSession() {
try {
const me = await api('/api/me');
loginScreen.classList.add('hidden');
mainScreen.classList.remove('hidden');
$('#server-info').textContent = `${me.username} @ ${me.nextcloudUrl}`;
await loadAll();
} catch {
loginScreen.classList.remove('hidden');
mainScreen.classList.add('hidden');
}
}
async function loadGroups() {
const { groups } = await api('/api/groups');
allGroups = groups || [];
const sel = $('#field-groups');
sel.innerHTML = allGroups.map((g) => `<option value="${escapeHtml(g)}">${escapeHtml(g)}</option>`).join('');
}
async function loadUsers() {
usersBody.innerHTML = '<tr><td colspan="8" class="muted center">Загрузка…</td></tr>';
const { users } = await api('/api/users');
allUsers = users;
if (!users.length) {
usersBody.innerHTML = '<tr><td colspan="8" class="muted center">Нет пользователей</td></tr>';
return;
}
usersBody.innerHTML = users.map((u) => `
<tr>
<td><strong>${escapeHtml(u.id)}</strong></td>
<td>${escapeHtml(u.displayName || '—')}</td>
<td>${escapeHtml(u.email || '—')}</td>
<td>${escapeHtml(String(u.quota || 'default'))}</td>
<td>${formatBytes(u.used)}</td>
<td>${(u.groups || []).map((g) => `<span class="group-tag">${escapeHtml(g)}</span>`).join('') || '—'}</td>
<td><span class="badge ${u.enabled ? 'on' : 'off'}">${u.enabled ? 'активен' : 'отключён'}</span></td>
<td class="row-actions">
<button class="secondary small btn-edit" data-id="${escapeHtml(u.id)}">Изменить</button>
<button class="danger small btn-del" data-id="${escapeHtml(u.id)}" ${u.id === 'admin' ? 'disabled title="Нельзя удалить admin"' : ''}>Удалить</button>
</td>
</tr>
`).join('');
document.querySelectorAll('.btn-edit').forEach((btn) => {
btn.addEventListener('click', () => openEdit(btn.dataset.id));
});
document.querySelectorAll('.btn-del').forEach((btn) => {
btn.addEventListener('click', () => deleteUser(btn.dataset.id));
});
}
async function loadAll() {
try {
await Promise.all([loadGroups(), loadUsers()]);
} catch (e) {
showError(e.message);
}
}
function getQuotaValue() {
const sel = $('#field-quota').value;
if (sel === 'custom') return $('#field-quota-custom').value.trim();
if (sel === 'none') return 'none';
if (sel === 'default') return 'default';
return sel;
}
function setQuotaSelect(quota) {
const q = String(quota || 'default');
const sel = $('#field-quota');
const opts = [...sel.options].map((o) => o.value);
if (q === 'none' || q === 'default') {
sel.value = q;
} else if (opts.includes(q)) {
sel.value = q;
} else {
sel.value = 'custom';
$('#field-quota-custom').value = q;
}
$('#custom-quota-wrap').classList.toggle('hidden', sel.value !== 'custom');
}
function openCreate() {
$('#dialog-title').textContent = 'Новый пользователь';
$('#edit-userid').value = '';
$('#field-userid').value = '';
$('#field-userid').disabled = false;
$('#field-password').value = '';
$('#field-password').required = true;
$('#password-hint').textContent = 'Обязателен для нового пользователя';
$('#field-displayname').value = '';
$('#field-email').value = '';
setQuotaSelect('default');
$('#field-enabled').checked = true;
$('#field-enabled-wrap').classList.add('hidden');
[...$('#field-groups').options].forEach((o) => { o.selected = o.value === 'users'; });
userDialog.showModal();
}
function openEdit(userid) {
const u = allUsers.find((x) => x.id === userid);
if (!u) return;
$('#dialog-title').textContent = `Редактировать: ${userid}`;
$('#edit-userid').value = userid;
$('#field-userid').value = userid;
$('#field-userid').disabled = true;
$('#field-password').value = '';
$('#field-password').required = false;
$('#password-hint').textContent = 'Оставьте пустым, чтобы не менять';
$('#field-displayname').value = u.displayName || '';
$('#field-email').value = u.email || '';
setQuotaSelect(u.quota);
$('#field-enabled').checked = u.enabled !== false;
$('#field-enabled-wrap').classList.remove('hidden');
const groups = u.groups || [];
[...$('#field-groups').options].forEach((o) => { o.selected = groups.includes(o.value); });
userDialog.showModal();
}
async function deleteUser(id) {
if (!confirm(`Удалить пользователя «${id}»? Файлы будут удалены.`)) return;
try {
await api(`/api/users/${encodeURIComponent(id)}`, { method: 'DELETE' });
showOk(`Пользователь ${id} удалён`);
await loadUsers();
} catch (e) {
showError(e.message);
}
}
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
loginError.classList.add('hidden');
try {
await api('/api/login', {
method: 'POST',
body: JSON.stringify({
username: $('#login-user').value.trim(),
password: $('#login-pass').value,
}),
});
await checkSession();
} catch {
loginError.textContent = 'Неверный логин или пароль';
loginError.classList.remove('hidden');
}
});
$('#btn-logout').addEventListener('click', async () => {
await api('/api/logout', { method: 'POST' });
location.reload();
});
$('#btn-refresh').addEventListener('click', loadAll);
$('#btn-create').addEventListener('click', openCreate);
$('#btn-cancel').addEventListener('click', () => userDialog.close());
$('#field-quota').addEventListener('change', () => {
$('#custom-quota-wrap').classList.toggle('hidden', $('#field-quota').value !== 'custom');
});
userForm.addEventListener('submit', async (e) => {
e.preventDefault();
const editId = $('#edit-userid').value;
const quota = getQuotaValue();
const selectedGroups = [...$('#field-groups').selectedOptions].map((o) => o.value);
try {
if (editId) {
const body = {
quota,
displayName: $('#field-displayname').value.trim(),
email: $('#field-email').value.trim(),
enabled: $('#field-enabled').checked,
};
const pass = $('#field-password').value;
if (pass) body.password = pass;
await api(`/api/users/${encodeURIComponent(editId)}`, {
method: 'PATCH',
body: JSON.stringify(body),
});
const u = allUsers.find((x) => x.id === editId);
const currentGroups = u?.groups || [];
for (const g of selectedGroups) {
if (!currentGroups.includes(g)) {
await api(`/api/users/${encodeURIComponent(editId)}/groups`, {
method: 'POST',
body: JSON.stringify({ groupid: g }),
});
}
}
for (const g of currentGroups) {
if (!selectedGroups.includes(g) && g !== 'admin') {
await api(`/api/users/${encodeURIComponent(editId)}/groups/${encodeURIComponent(g)}`, {
method: 'DELETE',
});
}
}
showOk(`Пользователь ${editId} обновлён`);
} else {
const userid = $('#field-userid').value.trim();
const password = $('#field-password').value;
if (!password) throw new Error('Укажите пароль');
await api('/api/users', {
method: 'POST',
body: JSON.stringify({ userid, password, quota, groups: selectedGroups }),
});
showOk(`Пользователь ${userid} создан`);
}
userDialog.close();
await loadAll();
} catch (err) {
showError(err.message);
}
});
checkSession();