const $ = (s) => document.querySelector(s); let me = null; function toast(msg) { const t = $('#toast'); t.textContent = msg; t.classList.add('show'); clearTimeout(toast._t); toast._t = setTimeout(() => t.classList.remove('show'), 2200); } async function api(path, opts = {}) { const res = await fetch(path, opts); if (res.status === 401) { location.href = '/login'; throw new Error('auth'); } if (res.status === 403) { location.href = '/'; throw new Error('forbidden'); } const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`); return data; } const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); $('#logoutBtn').addEventListener('click', async () => { await fetch('/api/logout', { method: 'POST' }); location.href = '/login'; }); // --- tabs ------------------------------------------------------------------ $('#tabs').addEventListener('click', (e) => { const btn = e.target.closest('button'); if (!btn) return; document.querySelectorAll('#tabs button').forEach((b) => b.classList.toggle('active', b === btn)); document.querySelectorAll('.tab-panel').forEach((p) => p.classList.toggle('active', p.id === `tab-${btn.dataset.tab}`)); if (btn.dataset.tab === 'logs') loadLogs(); if (btn.dataset.tab === 'mail') loadMail(); }); // --- users ----------------------------------------------------------------- async function loadUsers() { const users = await api('/api/users'); const tbody = $('#userTable tbody'); tbody.innerHTML = ''; for (const u of users) { const tr = document.createElement('tr'); const isMe = me && u.id === me.user.id; tr.innerHTML = ` ${esc(u.username)}${isMe ? ' (you)' : ''} ${esc(u.email) || '—'} ${u.role} ${new Date(u.createdAt).toLocaleDateString()}
`; tbody.appendChild(tr); } tbody.querySelectorAll('[data-role]').forEach((b) => b.addEventListener('click', async () => { const next = b.dataset.current === 'admin' ? 'user' : 'admin'; try { await api(`/api/users/${b.dataset.role}/role`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role: next }) }); loadUsers(); } catch (e) { toast(e.message); } })); tbody.querySelectorAll('[data-pw]').forEach((b) => b.addEventListener('click', async () => { const pw = prompt('New password for this user:'); if (!pw) return; try { await api(`/api/users/${b.dataset.pw}/password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }) }); toast('Password updated'); } catch (e) { toast(e.message); } })); tbody.querySelectorAll('[data-del]').forEach((b) => b.addEventListener('click', async () => { if (!confirm('Delete this user?')) return; try { await api(`/api/users/${b.dataset.del}`, { method: 'DELETE' }); loadUsers(); } catch (e) { toast(e.message); } })); } $('#nu_add').addEventListener('click', async () => { $('#nu_err').textContent = ''; try { await api('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: $('#nu_name').value, email: $('#nu_email').value, password: $('#nu_pass').value, role: $('#nu_role').value }) }); $('#nu_name').value = $('#nu_email').value = $('#nu_pass').value = ''; toast('User created'); loadUsers(); } catch (e) { $('#nu_err').textContent = e.message; } }); // --- mail ------------------------------------------------------------------ async function loadMail() { const c = await api('/api/mail'); $('#m_enabled').checked = c.enabled; $('#m_username').value = c.username || ''; $('#m_password').value = ''; $('#m_password').placeholder = c.hasPassword ? '•••••••• (unchanged)' : 'password'; $('#m_fromName').value = c.fromName || ''; $('#m_smtpHost').value = c.smtpHost; $('#m_smtpPort').value = c.smtpPort; $('#m_imapHost').value = c.imapHost; $('#m_imapPort').value = c.imapPort; $('#m_sentFolder').value = c.sentFolder; $('#m_saveToSent').checked = c.saveToSent; } $('#m_save').addEventListener('click', async () => { $('#m_err').textContent = ''; const body = { enabled: $('#m_enabled').checked, username: $('#m_username').value.trim(), password: $('#m_password').value, fromName: $('#m_fromName').value, smtpHost: $('#m_smtpHost').value.trim(), smtpPort: $('#m_smtpPort').value, imapHost: $('#m_imapHost').value.trim(), imapPort: $('#m_imapPort').value, sentFolder: $('#m_sentFolder').value.trim(), saveToSent: $('#m_saveToSent').checked }; try { await api('/api/mail', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); toast('Mail settings saved'); loadMail(); } catch (e) { $('#m_err').textContent = e.message; } }); $('#m_test').addEventListener('click', async () => { $('#m_err').textContent = ''; const to = $('#m_testTo').value.trim(); try { await api('/api/mail/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to }) }); toast('Test email sent'); } catch (e) { $('#m_err').textContent = e.message; } }); // --- logs ------------------------------------------------------------------ async function loadLogs() { const logs = await api('/api/logs'); const box = $('#logList'); if (!logs.length) { box.innerHTML = '
No activity logged yet.
'; return; } box.innerHTML = logs.map((l) => `
${new Date(l.ts).toLocaleString()} ${l.level || 'info'} ${esc(l.actor || '')} ${esc(l.message || '')}
`).join(''); } $('#logRefresh').addEventListener('click', loadLogs); $('#logClear').addEventListener('click', async () => { if (!confirm('Clear the activity log?')) return; await api('/api/logs', { method: 'DELETE' }); loadLogs(); }); // --- boot ------------------------------------------------------------------ (async () => { try { me = await api('/api/me'); } catch { return; } if (!me.authed) { location.href = '/login'; return; } if (me.user.role !== 'admin') { location.href = '/'; return; } loadUsers(); })();