bzl

self-hosted ephemeral community engine
Log | Files | Refs | README | LICENSE

commit e68e36d4e38cdff13f5357a14668df5f7866b977
parent 11ed816a57bd47a1d9ae0fa145e556eed4480fa8
Author: SageAzakaela <106701693+SageAzakaela@users.noreply.github.com>
Date:   Sat, 21 Feb 2026 14:05:25 -0700

mods can add plugins now

Diffstat:
MCLEAN_INSTALL/docs/PLUGINS.md | 6+++---
MCLEAN_INSTALL/public/app.js | 18+++++++++++-------
MCLEAN_INSTALL/server.js | 16++++++++--------
Mdocs/PLUGINS.md | 6+++---
Mpublic/app.js | 18+++++++++++-------
Mserver.js | 16++++++++--------
6 files changed, 44 insertions(+), 36 deletions(-)

diff --git a/CLEAN_INSTALL/docs/PLUGINS.md b/CLEAN_INSTALL/docs/PLUGINS.md @@ -2,11 +2,11 @@ This is the **minimal plugin system** used to ship optional features without forking the core app. -Plugins are **trusted code**: the owner installs them, and they can run both client-side and server-side logic. +Plugins are **trusted code**: moderators/owners install them, and they can run both client-side and server-side logic. -## Install / manage (Owner UI) +## Install / manage (Moderator/Owner UI) -1. Sign in as the `owner`. +1. Sign in as `owner` or `moderator`. 2. Open the **Instance** panel in the left sidebar. 3. Under **Plugins**: - Upload a plugin `.zip` (must contain a `plugin.json` manifest). diff --git a/CLEAN_INSTALL/public/app.js b/CLEAN_INSTALL/public/app.js @@ -5771,8 +5771,12 @@ function isOwnerUser() { return Boolean(loggedInUser && loggedInRole === "owner"); } +function canManagePlugins() { + return Boolean(loggedInUser && (loggedInRole === "owner" || loggedInRole === "moderator")); +} + function renderPluginsAdminHtml() { - if (!isOwnerUser()) return `<div class="muted small">Owner only.</div>`; + if (!canManagePlugins()) return `<div class="muted small">Moderator/owner only.</div>`; const status = pluginAdminStatus ? `<div class="small muted">${escapeHtml(pluginAdminStatus)}</div>` : ""; const busyLine = pluginAdminBusy ? `<div class="small muted">Working...</div>` : ""; const listHtml = !plugins.length @@ -5804,7 +5808,7 @@ function renderPluginsAdminHtml() { }) .join(""); return ` - <div class="small muted">Owner-only. Install optional plugins to extend your instance.</div> + <div class="small muted">Moderator/owner only. Install optional plugins to extend your instance.</div> <div class="pluginInstallRow" style="margin-top:10px"> <input data-pluginzip="1" type="file" accept=".zip,application/zip" /> <button data-plugininstall="1" class="ghost" type="button">Install</button> @@ -9974,7 +9978,7 @@ modBodyEl?.addEventListener("click", (e) => { const pluginReloadBtn = e.target.closest("button[data-pluginreload]"); if (pluginReloadBtn) { - if (!isOwnerUser()) return; + if (!canManagePlugins()) return; pluginAdminBusy = true; pluginAdminStatus = "Reloading plugins..."; renderModPanel(); @@ -9984,7 +9988,7 @@ modBodyEl?.addEventListener("click", (e) => { const pluginUninstallBtn = e.target.closest("button[data-pluginuninstall]"); if (pluginUninstallBtn) { - if (!isOwnerUser()) return; + if (!canManagePlugins()) return; const id = String(pluginUninstallBtn.getAttribute("data-pluginuninstall") || "").trim().toLowerCase(); if (!id) return; const ok = confirm(`Uninstall "${id}"? This deletes the plugin files from this server.`); @@ -9998,7 +10002,7 @@ modBodyEl?.addEventListener("click", (e) => { const pluginInstallBtn = e.target.closest("button[data-plugininstall]"); if (pluginInstallBtn) { - if (!isOwnerUser()) return; + if (!canManagePlugins()) return; const input = modBodyEl.querySelector("input[type='file'][data-pluginzip]") || null; const file = input?.files && input.files[0] ? input.files[0] : null; if (!file) { @@ -10293,7 +10297,7 @@ modBodyEl?.addEventListener("change", (e) => { const toggle = e.target?.closest?.("input[type='checkbox'][data-pluginenable]"); if (toggle) { - if (!isOwnerUser()) return; + if (!canManagePlugins()) return; const id = String(toggle.getAttribute("data-pluginenable") || "").trim().toLowerCase(); if (!id) return; const enabled = Boolean(toggle.checked); @@ -11182,7 +11186,7 @@ function onWsMessage(evt) { if (msg.type === "permissionDenied") { const m = msg.message || "Permission denied."; - if (/owner access required/i.test(m)) { + if (/(owner|moderator) access required/i.test(m)) { pluginAdminStatus = m; pluginAdminBusy = false; pluginEnableInFlight.clear(); diff --git a/CLEAN_INSTALL/server.js b/CLEAN_INSTALL/server.js @@ -3205,8 +3205,8 @@ async function handlePluginInstall(req, res, url) { return true; } const username = getSessionUserFromRequest(req); - if (!username || !hasRole(username, ROLE_OWNER)) { - sendJson(res, 403, { error: "Owner access required." }); + if (!username || !hasRole(username, ROLE_MODERATOR)) { + sendJson(res, 403, { error: "Moderator access required." }); return true; } @@ -5400,8 +5400,8 @@ wss.on("connection", (ws, req) => { if (msg.type === "pluginSetEnabled") { const actor = ws?.user?.username; - if (!actor || !hasRole(actor, ROLE_OWNER)) { - ws.send(JSON.stringify({ type: "permissionDenied", message: "Owner access required." })); + if (!actor || !hasRole(actor, ROLE_MODERATOR)) { + ws.send(JSON.stringify({ type: "permissionDenied", message: "Moderator access required." })); return; } const id = normalizePluginId(msg.id || msg.pluginId || ""); @@ -5425,8 +5425,8 @@ wss.on("connection", (ws, req) => { if (msg.type === "pluginUninstall") { const actor = ws?.user?.username; - if (!actor || !hasRole(actor, ROLE_OWNER)) { - ws.send(JSON.stringify({ type: "permissionDenied", message: "Owner access required." })); + if (!actor || !hasRole(actor, ROLE_MODERATOR)) { + ws.send(JSON.stringify({ type: "permissionDenied", message: "Moderator access required." })); return; } const id = normalizePluginId(msg.id || msg.pluginId || ""); @@ -5455,8 +5455,8 @@ wss.on("connection", (ws, req) => { if (msg.type === "pluginReload") { const actor = ws?.user?.username; - if (!actor || !hasRole(actor, ROLE_OWNER)) { - ws.send(JSON.stringify({ type: "permissionDenied", message: "Owner access required." })); + if (!actor || !hasRole(actor, ROLE_MODERATOR)) { + ws.send(JSON.stringify({ type: "permissionDenied", message: "Moderator access required." })); return; } loadPluginsFromDisk(); diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md @@ -2,7 +2,7 @@ This is the **minimal plugin system** used to ship optional features without forking the core app. -Plugins are **trusted code**: the owner installs them, and they can run both client-side and server-side logic. +Plugins are **trusted code**: moderators/owners install them, and they can run both client-side and server-side logic. ## Included plugins at a glance @@ -17,9 +17,9 @@ Use this quick reference when packaging releases or deciding what to enable by d | `directory-server` | Public directory endpoint + moderation queue for listings (`/api/plugins/directory-server/list`) | **Directory host instance only** | Acts as the central directory service. | | `directory-publisher` | Sends this instance announcement payloads to a directory server | Any instance that wants listing | Configure directory URL + instance URL/name, then publish. | -## Install / manage (Owner UI) +## Install / manage (Moderator/Owner UI) -1. Sign in as the `owner`. +1. Sign in as `owner` or `moderator`. 2. Open the **Instance** panel in the left sidebar. 3. Under **Plugins**: - Upload a plugin `.zip` (must contain a `plugin.json` manifest). diff --git a/public/app.js b/public/app.js @@ -5771,8 +5771,12 @@ function isOwnerUser() { return Boolean(loggedInUser && loggedInRole === "owner"); } +function canManagePlugins() { + return Boolean(loggedInUser && (loggedInRole === "owner" || loggedInRole === "moderator")); +} + function renderPluginsAdminHtml() { - if (!isOwnerUser()) return `<div class="muted small">Owner only.</div>`; + if (!canManagePlugins()) return `<div class="muted small">Moderator/owner only.</div>`; const status = pluginAdminStatus ? `<div class="small muted">${escapeHtml(pluginAdminStatus)}</div>` : ""; const busyLine = pluginAdminBusy ? `<div class="small muted">Working...</div>` : ""; const listHtml = !plugins.length @@ -5804,7 +5808,7 @@ function renderPluginsAdminHtml() { }) .join(""); return ` - <div class="small muted">Owner-only. Install optional plugins to extend your instance.</div> + <div class="small muted">Moderator/owner only. Install optional plugins to extend your instance.</div> <div class="pluginInstallRow" style="margin-top:10px"> <input data-pluginzip="1" type="file" accept=".zip,application/zip" /> <button data-plugininstall="1" class="ghost" type="button">Install</button> @@ -9974,7 +9978,7 @@ modBodyEl?.addEventListener("click", (e) => { const pluginReloadBtn = e.target.closest("button[data-pluginreload]"); if (pluginReloadBtn) { - if (!isOwnerUser()) return; + if (!canManagePlugins()) return; pluginAdminBusy = true; pluginAdminStatus = "Reloading plugins..."; renderModPanel(); @@ -9984,7 +9988,7 @@ modBodyEl?.addEventListener("click", (e) => { const pluginUninstallBtn = e.target.closest("button[data-pluginuninstall]"); if (pluginUninstallBtn) { - if (!isOwnerUser()) return; + if (!canManagePlugins()) return; const id = String(pluginUninstallBtn.getAttribute("data-pluginuninstall") || "").trim().toLowerCase(); if (!id) return; const ok = confirm(`Uninstall "${id}"? This deletes the plugin files from this server.`); @@ -9998,7 +10002,7 @@ modBodyEl?.addEventListener("click", (e) => { const pluginInstallBtn = e.target.closest("button[data-plugininstall]"); if (pluginInstallBtn) { - if (!isOwnerUser()) return; + if (!canManagePlugins()) return; const input = modBodyEl.querySelector("input[type='file'][data-pluginzip]") || null; const file = input?.files && input.files[0] ? input.files[0] : null; if (!file) { @@ -10293,7 +10297,7 @@ modBodyEl?.addEventListener("change", (e) => { const toggle = e.target?.closest?.("input[type='checkbox'][data-pluginenable]"); if (toggle) { - if (!isOwnerUser()) return; + if (!canManagePlugins()) return; const id = String(toggle.getAttribute("data-pluginenable") || "").trim().toLowerCase(); if (!id) return; const enabled = Boolean(toggle.checked); @@ -11182,7 +11186,7 @@ function onWsMessage(evt) { if (msg.type === "permissionDenied") { const m = msg.message || "Permission denied."; - if (/owner access required/i.test(m)) { + if (/(owner|moderator) access required/i.test(m)) { pluginAdminStatus = m; pluginAdminBusy = false; pluginEnableInFlight.clear(); diff --git a/server.js b/server.js @@ -3406,8 +3406,8 @@ async function handlePluginInstall(req, res, url) { return true; } const username = getSessionUserFromRequest(req); - if (!username || !hasRole(username, ROLE_OWNER)) { - sendJson(res, 403, { error: "Owner access required." }); + if (!username || !hasRole(username, ROLE_MODERATOR)) { + sendJson(res, 403, { error: "Moderator access required." }); return true; } @@ -5608,8 +5608,8 @@ wss.on("connection", (ws, req) => { if (msg.type === "pluginSetEnabled") { const actor = ws?.user?.username; - if (!actor || !hasRole(actor, ROLE_OWNER)) { - ws.send(JSON.stringify({ type: "permissionDenied", message: "Owner access required." })); + if (!actor || !hasRole(actor, ROLE_MODERATOR)) { + ws.send(JSON.stringify({ type: "permissionDenied", message: "Moderator access required." })); return; } const id = normalizePluginId(msg.id || msg.pluginId || ""); @@ -5633,8 +5633,8 @@ wss.on("connection", (ws, req) => { if (msg.type === "pluginUninstall") { const actor = ws?.user?.username; - if (!actor || !hasRole(actor, ROLE_OWNER)) { - ws.send(JSON.stringify({ type: "permissionDenied", message: "Owner access required." })); + if (!actor || !hasRole(actor, ROLE_MODERATOR)) { + ws.send(JSON.stringify({ type: "permissionDenied", message: "Moderator access required." })); return; } const id = normalizePluginId(msg.id || msg.pluginId || ""); @@ -5663,8 +5663,8 @@ wss.on("connection", (ws, req) => { if (msg.type === "pluginReload") { const actor = ws?.user?.username; - if (!actor || !hasRole(actor, ROLE_OWNER)) { - ws.send(JSON.stringify({ type: "permissionDenied", message: "Owner access required." })); + if (!actor || !hasRole(actor, ROLE_MODERATOR)) { + ws.send(JSON.stringify({ type: "permissionDenied", message: "Moderator access required." })); return; } loadPluginsFromDisk();