(() => { const menuController = window.WarpBoxUI?.bindMenuBar?.() || { close() {} }; const toastTarget = document.getElementById("toast"); const body = document.getElementById("users-body"); const search = document.getElementById("users-search"); const statusFilter = document.getElementById("users-status"); const sort = document.getElementById("users-sort"); const size = document.getElementById("users-size"); const masterCheck = document.getElementById("users-master-check"); const pageInfo = document.getElementById("users-page-info"); const visiblePill = document.getElementById("visible-pill"); const selectedPill = document.getElementById("users-selected-pill"); const prevBtn = document.getElementById("users-prev"); const nextBtn = document.getElementById("users-next"); const selectVisible = document.getElementById("select-visible"); const statusLeft = document.getElementById("users-status-left"); const selectedName = document.getElementById("selected-user-name"); const selectedMeta = document.getElementById("selected-user-meta"); const addForm = document.getElementById("add-user-form"); const editForm = document.getElementById("edit-user-form"); const policiesForm = document.getElementById("policies-form"); const apiKeyForm = document.getElementById("api-key-form"); const apiKeyList = document.getElementById("api-key-list"); const apiKeyReveal = document.getElementById("api-key-reveal"); const apiKeyValue = document.getElementById("api-key-value"); if (!body || !search || !statusFilter || !sort || !size) return; const state = { page: 1, users: [], selected: new Set(), currentUserID: "", }; const fields = { add: { username: document.getElementById("add-username"), email: document.getElementById("add-email"), status: document.getElementById("add-status"), maxFile: document.getElementById("add-max-file"), maxBox: document.getElementById("add-max-box"), web: document.getElementById("add-perm-web"), api: document.getElementById("add-perm-api"), create: document.getElementById("add-perm-create"), upload: document.getElementById("add-perm-upload"), }, edit: { username: document.getElementById("edit-username"), email: document.getElementById("edit-email"), status: document.getElementById("edit-status"), save: document.getElementById("save-edit-button"), delete: document.getElementById("delete-user-button"), }, policies: { maxFile: document.getElementById("policy-max-file"), maxBox: document.getElementById("policy-max-box"), web: document.getElementById("policy-perm-web"), api: document.getElementById("policy-perm-api"), create: document.getElementById("policy-perm-create"), upload: document.getElementById("policy-perm-upload"), save: document.getElementById("save-policies-button"), }, keys: { name: document.getElementById("api-key-name"), create: document.getElementById("create-key-button"), }, }; function toast(message, type = "info") { if (window.WarpBoxUI) { window.WarpBoxUI.toast(message, type, { target: toastTarget, duration: 3200 }); return; } if (toastTarget) toastTarget.textContent = message; } function escapeHTML(value) { return String(value || "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } async function api(path, method = "GET", payload = null) { const response = await fetch(path, { method, headers: payload ? { "Content-Type": "application/json" } : undefined, body: payload ? JSON.stringify(payload) : undefined, }); let data = {}; try { data = await response.json(); } catch (_) {} if (!response.ok) throw new Error(data.error || "Request failed"); return data; } function selectedUser() { return state.users.find((user) => user.id === state.currentUserID) || null; } const BYTES_PER_MB = 1024 * 1024; function numericMB(input) { const value = Number(input?.value || 0); return Number.isFinite(value) && value > 0 ? String(Math.floor(value)) : "0"; } function bytesToMB(value) { const bytes = Number(value || 0); return Number.isFinite(bytes) && bytes > 0 ? String(Math.ceil(bytes / BYTES_PER_MB)) : "0"; } function limitLabelMB(value) { const mb = Number(bytesToMB(value)); return mb > 0 ? `${mb} MB` : "unlimited"; } function permissionPayload(source) { return { can_use_web: Boolean(source.web?.checked), can_use_api: Boolean(source.api?.checked), can_create_box: Boolean(source.create?.checked), can_upload_file: Boolean(source.upload?.checked), }; } function payloadFromUser(user, overrides = {}) { return { id: user.id, username: user.username, email: user.email, status: user.status, max_file_size_mb: bytesToMB(user.limits?.max_file_size_bytes), max_box_size_mb: bytesToMB(user.limits?.max_box_size_bytes), permissions: user.permissions || {}, ...overrides, }; } function setTab(tabName) { document.querySelectorAll(".users-tab").forEach((tab) => { tab.classList.toggle("is-active", tab.dataset.tab === tabName); }); document.querySelectorAll(".users-tab-panel").forEach((panel) => { panel.classList.toggle("is-active", panel.dataset.panel === tabName); }); } function setSelectedUser(userID, preferredTab = "edit") { state.currentUserID = userID || ""; state.selected.clear(); if (userID) state.selected.add(userID); populateSelectedPanels(); render(); if (preferredTab) setTab(preferredTab); } function setControlsEnabled(group, enabled) { Object.values(group).forEach((element) => { if (!element) return; element.disabled = !enabled; }); } function populateSelectedPanels() { const user = selectedUser(); const hasUser = Boolean(user); selectedName.textContent = hasUser ? user.username : "None"; selectedMeta.textContent = hasUser ? `${user.email} · ${user.status}` : "Choose a row to edit policies and keys."; setControlsEnabled(fields.edit, hasUser); setControlsEnabled(fields.policies, hasUser); setControlsEnabled(fields.keys, hasUser); if (!hasUser) { fields.edit.username.value = ""; fields.edit.email.value = ""; fields.edit.status.value = "active"; fields.policies.maxFile.value = ""; fields.policies.maxBox.value = ""; [fields.policies.web, fields.policies.api, fields.policies.create, fields.policies.upload].forEach((item) => { item.checked = false; }); fields.keys.name.value = "default"; apiKeyList.innerHTML = `

Select a user to manage API keys.

`; apiKeyReveal.hidden = true; return; } fields.edit.username.value = user.username || ""; fields.edit.email.value = user.email || ""; fields.edit.status.value = user.status || "active"; fields.policies.maxFile.value = bytesToMB(user.limits?.max_file_size_bytes); fields.policies.maxBox.value = bytesToMB(user.limits?.max_box_size_bytes); fields.policies.web.checked = Boolean(user.permissions?.can_use_web); fields.policies.api.checked = Boolean(user.permissions?.can_use_api); fields.policies.create.checked = Boolean(user.permissions?.can_create_box); fields.policies.upload.checked = Boolean(user.permissions?.can_upload_file); fields.keys.name.value = "default"; renderAPIKeys(user); } function renderAPIKeys(user) { const keys = user.api_keys || []; if (!keys.length) { apiKeyList.innerHTML = `

No API keys yet.

`; return; } apiKeyList.innerHTML = keys.map((key) => { const revoked = Boolean(key.revoked_at); return `
${escapeHTML(key.name || "default")}${escapeHTML(key.prefix || key.id)}${revoked ? " · revoked" : ""}
`; }).join(""); apiKeyList.querySelectorAll("[data-revoke-key]").forEach((button) => { button.addEventListener("click", () => revokeAPIKey(button.dataset.revokeKey)); }); } function renderStats() { document.getElementById("stat-total").textContent = String(state.users.length); document.getElementById("stat-active").textContent = String(state.users.filter((u) => u.status === "active").length); document.getElementById("stat-keys").textContent = String(state.users.filter((u) => (u.api_key_count || 0) > 0).length); document.getElementById("stat-disabled").textContent = String(state.users.filter((u) => u.status === "disabled").length); } function filteredUsers() { const query = search.value.trim().toLowerCase(); const currentStatus = statusFilter.value; const rows = state.users.filter((user) => { const matchesQuery = !query || user.username.toLowerCase().includes(query) || user.email.toLowerCase().includes(query); const matchesStatus = currentStatus === "all" || user.status === currentStatus; return matchesQuery && matchesStatus; }); rows.sort((a, b) => { if (sort.value === "createdDesc") return String(b.created_at).localeCompare(String(a.created_at)); if (sort.value === "lastSeenDesc") return String(b.last_seen_at || "").localeCompare(String(a.last_seen_at || "")); if (sort.value === "keysDesc") return (b.api_key_count || 0) - (a.api_key_count || 0); return a.username.localeCompare(b.username); }); return rows; } function paged(rows) { const perPage = Number(size.value || 12); const pages = Math.max(1, Math.ceil(rows.length / perPage)); state.page = Math.max(1, Math.min(state.page, pages)); const start = (state.page - 1) * perPage; return { rows: rows.slice(start, start + perPage), pages }; } function permissionsSummary(permissions = {}) { const items = []; if (permissions.can_use_web) items.push("web"); if (permissions.can_use_api) items.push("api"); if (permissions.can_create_box) items.push("create"); if (permissions.can_upload_file) items.push("upload"); return items.join(", ") || "none"; } function limitsSummary(limits = {}) { return `file ${limitLabelMB(limits.max_file_size_bytes)} / box ${limitLabelMB(limits.max_box_size_bytes)}`; } function renderRow(user) { const checked = state.selected.has(user.id) ? " checked" : ""; const active = user.id === state.currentUserID ? " is-selected" : ""; const row = document.createElement("tr"); row.className = active; row.dataset.userId = user.id; row.innerHTML = `
${escapeHTML(user.username)}${escapeHTML(user.id)}
${escapeHTML(user.email)} ${escapeHTML(user.status)} ${escapeHTML(permissionsSummary(user.permissions))} ${escapeHTML(limitsSummary(user.limits))} ${user.api_key_count || 0} ${escapeHTML(user.last_seen_at || "never")}
`; row.addEventListener("click", (event) => { if (event.target.closest(".row-check") || event.target.closest("button")) return; setSelectedUser(user.id, "edit"); }); row.querySelector(".row-check")?.addEventListener("change", (event) => { if (event.target.checked) state.selected.add(user.id); else state.selected.delete(user.id); if (event.target.checked) state.currentUserID = user.id; populateSelectedPanels(); syncSelected(); syncMasterCheck(); render(); }); row.querySelector('[data-action="edit"]')?.addEventListener("click", () => setSelectedUser(user.id, "edit")); row.querySelector('[data-action="keys"]')?.addEventListener("click", () => setSelectedUser(user.id, "keys")); return row; } function syncSelected() { selectedPill.textContent = `${state.selected.size} selected`; } function syncMasterCheck() { const checks = Array.from(body.querySelectorAll(".row-check")); masterCheck.checked = checks.length > 0 && checks.every((item) => item.checked); } function render() { const rows = filteredUsers(); const page = paged(rows); body.innerHTML = ""; page.rows.forEach((user) => body.appendChild(renderRow(user))); visiblePill.textContent = `${rows.length} visible`; pageInfo.textContent = `Page ${state.page} / ${page.pages}`; prevBtn.disabled = state.page <= 1; nextBtn.disabled = state.page >= page.pages; statusLeft.textContent = `Ready. ${rows.length} user rows in current filter.`; syncSelected(); syncMasterCheck(); } async function fetchUsers() { const result = await api("/admin/users/list"); state.users = result.users || []; if (state.currentUserID && !state.users.some((user) => user.id === state.currentUserID)) { state.currentUserID = ""; } state.selected = new Set([...state.selected].filter((id) => state.users.some((user) => user.id === id))); renderStats(); populateSelectedPanels(); render(); } async function saveUser(payload, successMessage) { const result = await api("/admin/users/save", "POST", payload); state.currentUserID = result.user?.id || state.currentUserID; state.selected.clear(); if (state.currentUserID) state.selected.add(state.currentUserID); await fetchUsers(); toast(successMessage); return result.user; } async function deleteUser(userID) { const user = state.users.find((item) => item.id === userID); if (!user) return; if (!confirm(`Delete ${user.username}?`)) return; await api("/admin/users/delete", "POST", { id: userID }); state.currentUserID = ""; state.selected.delete(userID); await fetchUsers(); setTab("add"); toast("User deleted"); } async function revokeAPIKey(keyID) { const user = selectedUser(); if (!user || !keyID) return; await api("/admin/users/api-keys/revoke", "POST", { user_id: user.id, key_id: keyID }); await fetchUsers(); setTab("keys"); toast("API key revoked"); } async function runCommand(command) { switch (command) { case "tab-add": case "create": setTab("add"); break; case "refresh": await fetchUsers(); toast("Users list refreshed"); break; case "bulk-disable": case "bulk-enable": { const nextStatus = command === "bulk-disable" ? "disabled" : "active"; const ids = Array.from(state.selected); if (!ids.length) { toast("Select one or more users first", "warning"); return; } for (const id of ids) { const user = state.users.find((item) => item.id === id); if (!user) continue; await api("/admin/users/save", "POST", payloadFromUser(user, { status: nextStatus })); } await fetchUsers(); toast(`Updated ${ids.length} user(s)`); break; } case "bulk-delete": { const ids = Array.from(state.selected); if (!ids.length) { toast("Select one or more users first", "warning"); return; } if (!confirm(`Delete ${ids.length} selected user(s)?`)) return; for (const id of ids) await api("/admin/users/delete", "POST", { id }); state.selected.clear(); state.currentUserID = ""; await fetchUsers(); setTab("add"); toast(`Deleted ${ids.length} user(s)`); break; } case "clear-filters": search.value = ""; statusFilter.value = "all"; sort.value = "username"; state.page = 1; render(); break; default: break; } } document.querySelectorAll(".users-tab").forEach((tab) => { tab.addEventListener("click", () => setTab(tab.dataset.tab)); }); [search, statusFilter, sort, size].forEach((element) => { element.addEventListener(element.tagName === "INPUT" ? "input" : "change", () => { state.page = 1; render(); }); }); prevBtn.addEventListener("click", () => { state.page -= 1; render(); }); nextBtn.addEventListener("click", () => { state.page += 1; render(); }); masterCheck.addEventListener("change", () => { Array.from(body.querySelectorAll("tr")).forEach((row) => { const userID = row.dataset.userId || ""; const checkbox = row.querySelector(".row-check"); if (!checkbox || !userID) return; checkbox.checked = masterCheck.checked; if (masterCheck.checked) state.selected.add(userID); else state.selected.delete(userID); }); if (state.selected.size === 1) state.currentUserID = Array.from(state.selected)[0]; populateSelectedPanels(); render(); }); selectVisible.addEventListener("click", () => { Array.from(body.querySelectorAll("tr")).forEach((row) => { const userID = row.dataset.userId || ""; const checkbox = row.querySelector(".row-check"); if (!checkbox || !userID) return; checkbox.checked = true; state.selected.add(userID); }); if (state.selected.size === 1) state.currentUserID = Array.from(state.selected)[0]; populateSelectedPanels(); render(); }); addForm.addEventListener("submit", async (event) => { event.preventDefault(); const username = fields.add.username.value.trim(); const email = fields.add.email.value.trim(); if (!username || !email) { toast("Username and email are required", "warning"); return; } try { await saveUser({ username, email, status: fields.add.status.value, max_file_size_mb: numericMB(fields.add.maxFile), max_box_size_mb: numericMB(fields.add.maxBox), permissions: permissionPayload(fields.add), }, "User created"); addForm.reset(); fields.add.status.value = "active"; fields.add.maxFile.value = "0"; fields.add.maxBox.value = "0"; [fields.add.web, fields.add.api, fields.add.create, fields.add.upload].forEach((item) => { item.checked = true; }); setTab("edit"); } catch (error) { toast(error.message || "Could not create user", "warning"); } }); editForm.addEventListener("submit", async (event) => { event.preventDefault(); const user = selectedUser(); if (!user) return; try { await saveUser(payloadFromUser(user, { username: fields.edit.username.value.trim(), email: fields.edit.email.value.trim(), status: fields.edit.status.value, }), "User updated"); } catch (error) { toast(error.message || "Could not update user", "warning"); } }); fields.edit.delete.addEventListener("click", () => { const user = selectedUser(); if (user) deleteUser(user.id); }); policiesForm.addEventListener("submit", async (event) => { event.preventDefault(); const user = selectedUser(); if (!user) return; try { await saveUser(payloadFromUser(user, { max_file_size_mb: numericMB(fields.policies.maxFile), max_box_size_mb: numericMB(fields.policies.maxBox), permissions: permissionPayload(fields.policies), }), "Policies updated"); } catch (error) { toast(error.message || "Could not update policies", "warning"); } }); apiKeyForm.addEventListener("submit", async (event) => { event.preventDefault(); const user = selectedUser(); if (!user) return; try { const result = await api("/admin/users/api-keys/create", "POST", { user_id: user.id, name: fields.keys.name.value.trim() || "default", }); apiKeyReveal.hidden = false; apiKeyValue.value = result.api_key || ""; await fetchUsers(); setTab("keys"); toast("API key generated"); } catch (error) { toast(error.message || "Could not generate API key", "warning"); } }); document.querySelectorAll("[data-command]").forEach((button) => { button.addEventListener("click", async () => { menuController.close(); try { await runCommand(button.dataset.command); } catch (error) { toast(error.message || "Action failed", "warning"); } }); }); document.addEventListener("keydown", async (event) => { if (event.key === "Escape") menuController.close(); if (event.key === "F5") { event.preventDefault(); await runCommand("refresh"); } }); fetchUsers().catch((error) => toast(error.message || "Failed to load users", "warning")); })();