// ───────────────────────────────────────────────────────────── // admin/admin.js — pulls invites + joins from the gated // /api/fenjaops endpoints and renders them. Also wires up the // "Invite a new user" form (POST /api/fenjaops/invites), which // only creates non-admin invites. Promoting to admin stays on // the CLI (bin/invite.js admin add) by design. // // Note: the public URL path is `/fenjaops` (not `/admin`) as a // small obscurity measure. Internal names stay as "admin". // ───────────────────────────────────────────────────────────── function iso(ms) { return new Date(ms).toISOString().replace('T', ' ').slice(0, 19) + 'Z'; } function shortSession(id) { if (!id) return ''; return id.slice(0, 10) + '…'; } function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); } function renderInvites(rows) { const tbody = document.querySelector('#t-invites tbody'); const empty = document.getElementById('empty-invites'); if (!rows.length) { tbody.innerHTML = ''; empty.hidden = false; return; } empty.hidden = true; tbody.innerHTML = rows.map((r) => { const when = new Date(r.invited_at).toISOString().slice(0, 10); const admin = r.is_admin ? 'Admin' : ''; return `
Failed to load admin data — check the server logs.
'; console.error('[admin]', err); } } const ERROR_COPY = { invalid_email: 'That email address is not valid.', invalid_first_name: 'First name is invalid.', first_name_too_long: 'First name is too long (max 64 characters).', already_invited: 'That email is already on the invite list.', }; function setupInviteForm() { const form = document.getElementById('invite-form'); const msg = document.getElementById('invite-msg'); if (!form || !msg) return; function show(text, cls) { msg.textContent = text; msg.classList.remove('ok', 'err'); msg.classList.add(cls); msg.hidden = false; } form.addEventListener('submit', async (ev) => { ev.preventDefault(); msg.hidden = true; const data = new FormData(form); const email = String(data.get('email') || '').trim(); const firstName = String(data.get('first_name') || '').trim(); const body = { email }; if (firstName) body.first_name = firstName; const btn = form.querySelector('button[type="submit"]'); btn.disabled = true; try { const res = await fetch('/api/fenjaops/invites', { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const payload = await res.json().catch(() => ({})); if (res.status === 201) { show(`Invited ${payload.email}.`, 'ok'); form.reset(); load(); return; } if (res.status === 401 || res.status === 404) { window.location.href = '/'; return; } const code = payload.error || 'error'; show(ERROR_COPY[code] || `Error: ${code}`, 'err'); } catch (err) { console.error('[admin] invite submit', err); show('Network error — check server logs.', 'err'); } finally { btn.disabled = false; } }); } load(); setupInviteForm();