283 lines
9.0 KiB
JavaScript
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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();
|