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) => ``).join(''); } async function loadUsers() { usersBody.innerHTML = 'Загрузка…'; const { users } = await api('/api/users'); allUsers = users; if (!users.length) { usersBody.innerHTML = 'Нет пользователей'; return; } usersBody.innerHTML = users.map((u) => ` ${escapeHtml(u.id)} ${escapeHtml(u.displayName || '—')} ${escapeHtml(u.email || '—')} ${escapeHtml(String(u.quota || 'default'))} ${formatBytes(u.used)} ${(u.groups || []).map((g) => `${escapeHtml(g)}`).join('') || '—'} ${u.enabled ? 'активен' : 'отключён'} `).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();