new plugin signing and checking
This commit is contained in:
@@ -309,6 +309,89 @@ async function loadCurrentUser() {
|
||||
}
|
||||
}
|
||||
|
||||
function showPluginConfirmModal(pluginId, clientId, sigInfo) {
|
||||
document.getElementById("plugin-confirm-modal")?.remove();
|
||||
|
||||
const isSigned = sigInfo && sigInfo.signed && sigInfo.valid;
|
||||
const statusText = isSigned
|
||||
? `This plugin is signed but the signer's key is not trusted.`
|
||||
: `This plugin is not signed and its origin cannot be verified.`;
|
||||
const fpText = sigInfo?.fingerprint
|
||||
? `Signer fingerprint: ${sigInfo.fingerprint}`
|
||||
: "No signature present";
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "plugin-confirm-modal";
|
||||
modal.className = "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm";
|
||||
modal.innerHTML = `
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl max-w-md w-full mx-4 p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center ${isSigned ? 'bg-yellow-900/60 text-yellow-400' : 'bg-orange-900/60 text-orange-400'}">
|
||||
<i class="fa-solid ${isSigned ? 'fa-shield' : 'fa-shield-halved'} text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg">${isSigned ? 'Untrusted Plugin' : 'Unsigned Plugin'}</h3>
|
||||
<p class="text-sm text-slate-400">${pluginId}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-slate-300 mb-2">${statusText}</p>
|
||||
<p class="text-xs text-slate-500 font-mono mb-4">${fpText}</p>
|
||||
<p class="text-sm text-slate-300 mb-3">Type <strong class="text-white">confirm</strong> below to load this plugin:</p>
|
||||
<input
|
||||
id="plugin-confirm-input"
|
||||
type="text"
|
||||
placeholder="Type confirm…"
|
||||
autocomplete="off"
|
||||
class="w-full px-3 py-2 rounded-lg bg-slate-950 border border-slate-700 text-slate-100 text-sm placeholder-slate-500 focus:outline-none focus:border-emerald-600 mb-4"
|
||||
/>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button id="plugin-confirm-cancel" class="px-4 py-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-800">Cancel</button>
|
||||
<button id="plugin-confirm-load" disabled class="px-4 py-2 rounded-lg bg-emerald-900/40 border border-emerald-700/60 text-emerald-100 opacity-50 cursor-not-allowed">Load Anyway</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const input = document.getElementById("plugin-confirm-input");
|
||||
const loadBtn = document.getElementById("plugin-confirm-load");
|
||||
const cancelBtn = document.getElementById("plugin-confirm-cancel");
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const match = input.value.trim().toLowerCase() === "confirm";
|
||||
loadBtn.disabled = !match;
|
||||
loadBtn.classList.toggle("opacity-50", !match);
|
||||
loadBtn.classList.toggle("cursor-not-allowed", !match);
|
||||
loadBtn.classList.toggle("hover:bg-emerald-800/60", match);
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener("click", () => modal.remove());
|
||||
modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); });
|
||||
|
||||
loadBtn.addEventListener("click", async () => {
|
||||
if (loadBtn.disabled) return;
|
||||
loadBtn.disabled = true;
|
||||
loadBtn.textContent = "Loading…";
|
||||
try {
|
||||
const res = await fetch(`/api/clients/${clientId}/plugins/${pluginId}/load`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ confirmed: true }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
alert(`Plugin load failed: ${text}`);
|
||||
} else {
|
||||
window.open(`/plugins/${pluginId}?clientId=${clientId}`, "_blank", "noopener");
|
||||
}
|
||||
} catch {
|
||||
alert("Plugin load failed");
|
||||
}
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
input.focus();
|
||||
}
|
||||
|
||||
async function loadPluginsForClient(clientId) {
|
||||
const section = document.getElementById("plugin-section");
|
||||
const container = document.getElementById("plugin-menu");
|
||||
@@ -336,11 +419,38 @@ async function loadPluginsForClient(clientId) {
|
||||
if (plugin.lastError) {
|
||||
btn.title = `Last error: ${plugin.lastError}`;
|
||||
}
|
||||
|
||||
const sig = plugin.signature;
|
||||
if (sig && sig.signed && !sig.valid) {
|
||||
btn.disabled = true;
|
||||
btn.classList.add("opacity-50", "cursor-not-allowed");
|
||||
btn.title = "Plugin signature is invalid — cannot load";
|
||||
}
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.className = "flex items-center gap-1";
|
||||
const labelIcon = document.createElement("i");
|
||||
labelIcon.className = "fa-solid fa-puzzle-piece";
|
||||
label.appendChild(labelIcon);
|
||||
label.append(` ${plugin.name || plugin.id}`);
|
||||
|
||||
if (sig) {
|
||||
const trustIcon = document.createElement("i");
|
||||
if (sig.signed && !sig.valid) {
|
||||
trustIcon.className = "fa-solid fa-shield-xmark text-red-400 text-xs ml-1";
|
||||
trustIcon.title = "Invalid signature";
|
||||
} else if (sig.signed && sig.valid && sig.trusted) {
|
||||
trustIcon.className = "fa-solid fa-shield-check text-emerald-400 text-xs ml-1";
|
||||
trustIcon.title = "Trusted";
|
||||
} else if (sig.signed && sig.valid && !sig.trusted) {
|
||||
trustIcon.className = "fa-solid fa-shield text-yellow-400 text-xs ml-1";
|
||||
trustIcon.title = "Signed but untrusted";
|
||||
} else {
|
||||
trustIcon.className = "fa-solid fa-shield-halved text-orange-400 text-xs ml-1";
|
||||
trustIcon.title = "Unsigned";
|
||||
}
|
||||
label.appendChild(trustIcon);
|
||||
}
|
||||
const badge = document.createElement("span");
|
||||
badge.className =
|
||||
"text-[10px] uppercase tracking-wider px-2 py-0.5 rounded-full border" +
|
||||
@@ -777,17 +887,35 @@ menu.addEventListener("click", async (e) => {
|
||||
}
|
||||
const pluginId = target.dataset.plugin;
|
||||
if (pluginId) {
|
||||
const savedClientId = contextCard;
|
||||
try {
|
||||
const res = await fetch(`/api/clients/${contextCard}/plugins/${pluginId}/load`, {
|
||||
const res = await fetch(`/api/clients/${savedClientId}/plugins/${pluginId}/load`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
if (res.status === 428) {
|
||||
const data = await res.json();
|
||||
closeMenu(clearContext);
|
||||
showPluginConfirmModal(pluginId, savedClientId, data.signature);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.status === 403) {
|
||||
const data = await res.json();
|
||||
alert(data.error || "Plugin load blocked — invalid signature");
|
||||
closeMenu(clearContext);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
alert(`Plugin load failed: ${text}`);
|
||||
closeMenu(clearContext);
|
||||
return;
|
||||
}
|
||||
window.open(`/plugins/${pluginId}?clientId=${contextCard}`, "_blank", "noopener");
|
||||
window.open(`/plugins/${pluginId}?clientId=${savedClientId}`, "_blank", "noopener");
|
||||
} catch (err) {
|
||||
alert("Plugin load failed");
|
||||
}
|
||||
|
||||
@@ -88,6 +88,14 @@ async function fetchPlugins() {
|
||||
return Array.isArray(data.plugins) ? data.plugins : [];
|
||||
}
|
||||
|
||||
function getTrustBadge(sig) {
|
||||
if (!sig) return { icon: "fa-shield-halved", color: "text-orange-400 border-orange-600 bg-orange-900/30", label: "Unsigned", tooltip: "This plugin is not signed" };
|
||||
if (sig.signed && !sig.valid) return { icon: "fa-shield-xmark", color: "text-red-400 border-red-600 bg-red-900/30", label: "Invalid", tooltip: "Signature verification failed — plugin may be tampered" };
|
||||
if (sig.signed && sig.valid && sig.trusted) return { icon: "fa-shield-check", color: "text-emerald-400 border-emerald-600 bg-emerald-900/30", label: "Trusted", tooltip: `Signed by trusted key: ${sig.fingerprint || "unknown"}` };
|
||||
if (sig.signed && sig.valid && !sig.trusted) return { icon: "fa-shield", color: "text-yellow-400 border-yellow-600 bg-yellow-900/30", label: "Untrusted", tooltip: `Signed but key not trusted: ${sig.fingerprint || "unknown"}` };
|
||||
return { icon: "fa-shield-halved", color: "text-orange-400 border-orange-600 bg-orange-900/30", label: "Unsigned", tooltip: "This plugin is not signed" };
|
||||
}
|
||||
|
||||
function renderPlugins(plugins) {
|
||||
pluginList.innerHTML = "";
|
||||
if (!plugins.length) {
|
||||
@@ -100,13 +108,34 @@ function renderPlugins(plugins) {
|
||||
card.className =
|
||||
"rounded-xl border border-slate-800 bg-slate-950/50 px-4 py-3 flex items-center justify-between";
|
||||
const meta = document.createElement("div");
|
||||
const title = document.createElement("div");
|
||||
const titleRow = document.createElement("div");
|
||||
titleRow.className = "flex items-center gap-2";
|
||||
const title = document.createElement("span");
|
||||
title.className = "font-semibold";
|
||||
title.textContent = plugin.name || plugin.id;
|
||||
titleRow.appendChild(title);
|
||||
|
||||
// Trust badge
|
||||
const badge = getTrustBadge(plugin.signature);
|
||||
const trustBadge = document.createElement("span");
|
||||
trustBadge.className = `inline-flex items-center gap-1 text-[10px] uppercase tracking-wider px-2 py-0.5 rounded-full border ${badge.color}`;
|
||||
trustBadge.innerHTML = `<i class="fa-solid ${badge.icon}"></i> ${badge.label}`;
|
||||
trustBadge.title = badge.tooltip;
|
||||
titleRow.appendChild(trustBadge);
|
||||
|
||||
const subtitle = document.createElement("div");
|
||||
subtitle.className = "text-sm text-slate-400";
|
||||
subtitle.textContent = `${plugin.id}${plugin.version ? ` • v${plugin.version}` : ""}`;
|
||||
meta.appendChild(title);
|
||||
|
||||
if (plugin.signature?.fingerprint) {
|
||||
const fpSpan = document.createElement("span");
|
||||
fpSpan.className = "ml-2 text-xs text-slate-500 font-mono";
|
||||
fpSpan.textContent = `${plugin.signature.fingerprint.slice(0, 16)}…`;
|
||||
fpSpan.title = `Signer fingerprint: ${plugin.signature.fingerprint}`;
|
||||
subtitle.appendChild(fpSpan);
|
||||
}
|
||||
|
||||
meta.appendChild(titleRow);
|
||||
meta.appendChild(subtitle);
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "flex items-center gap-2";
|
||||
@@ -121,47 +150,65 @@ function renderPlugins(plugins) {
|
||||
? '<i class="fa-solid fa-toggle-on"></i> Enabled'
|
||||
: '<i class="fa-solid fa-toggle-off"></i> Disabled';
|
||||
toggle.addEventListener("click", async () => {
|
||||
const res = await fetch(`/api/plugins/${plugin.id}/enable`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled: !plugin.enabled }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
setStatus(`Enable failed: ${text}`, true);
|
||||
return;
|
||||
const wantEnabled = !plugin.enabled;
|
||||
try {
|
||||
const res = await fetch(`/api/plugins/${plugin.id}/enable`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled: wantEnabled }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
if (data && data.error === "confirmation_required") {
|
||||
showPluginEnableConfirmModal(plugin, data.signature);
|
||||
return;
|
||||
}
|
||||
setStatus(`Enable failed: ${data?.error || res.statusText}`, true);
|
||||
return;
|
||||
}
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
setStatus("Enable failed", true);
|
||||
}
|
||||
await refresh();
|
||||
});
|
||||
|
||||
const autoLoadBtn = document.createElement("button");
|
||||
const autoLoadDisabled = !plugin.enabled;
|
||||
autoLoadBtn.className =
|
||||
"inline-flex items-center gap-2 px-3 py-2 rounded-lg border" +
|
||||
(plugin.autoLoad
|
||||
? " border-amber-600 text-amber-200 bg-amber-900/40"
|
||||
: " border-slate-600 text-slate-300 bg-slate-800/60");
|
||||
(autoLoadDisabled
|
||||
? " border-slate-700 text-slate-500 bg-slate-900/40 cursor-not-allowed opacity-50"
|
||||
: plugin.autoLoad
|
||||
? " border-amber-600 text-amber-200 bg-amber-900/40"
|
||||
: " border-slate-600 text-slate-300 bg-slate-800/60");
|
||||
autoLoadBtn.innerHTML = plugin.autoLoad
|
||||
? '<i class="fa-solid fa-bolt"></i> Auto-load'
|
||||
: '<i class="fa-solid fa-bolt-lightning"></i> Auto-load off';
|
||||
autoLoadBtn.title = plugin.autoLoad
|
||||
? "Plugin will auto-load on all new client connections. Click to disable."
|
||||
: "Click to auto-load this plugin on all new client connections.";
|
||||
autoLoadBtn.addEventListener("click", async () => {
|
||||
const res = await fetch(`/api/plugins/${plugin.id}/autoload`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
autoLoad: !plugin.autoLoad,
|
||||
autoStartEvents: plugin.autoStartEvents || [],
|
||||
}),
|
||||
autoLoadBtn.title = autoLoadDisabled
|
||||
? "Plugin must be enabled before auto-load can be turned on"
|
||||
: plugin.autoLoad
|
||||
? "Plugin will auto-load on all new client connections. Click to disable."
|
||||
: "Click to auto-load this plugin on all new client connections.";
|
||||
if (autoLoadDisabled) {
|
||||
autoLoadBtn.disabled = true;
|
||||
} else {
|
||||
autoLoadBtn.addEventListener("click", async () => {
|
||||
const res = await fetch(`/api/plugins/${plugin.id}/autoload`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
autoLoad: !plugin.autoLoad,
|
||||
autoStartEvents: plugin.autoStartEvents || [],
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
setStatus(`Auto-load toggle failed: ${text}`, true);
|
||||
return;
|
||||
}
|
||||
await refresh();
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
setStatus(`Auto-load toggle failed: ${text}`, true);
|
||||
return;
|
||||
}
|
||||
await refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.className =
|
||||
@@ -194,6 +241,80 @@ function renderPlugins(plugins) {
|
||||
}
|
||||
}
|
||||
|
||||
function showPluginEnableConfirmModal(plugin, sigInfo) {
|
||||
document.getElementById("plugin-enable-confirm-modal")?.remove();
|
||||
|
||||
const sig = sigInfo || {};
|
||||
let statusText = "This plugin is unsigned.";
|
||||
let statusColor = "text-orange-400";
|
||||
if (sig.signed && sig.valid && !sig.trusted) {
|
||||
statusText = "This plugin is signed but the signing key is not trusted.";
|
||||
statusColor = "text-yellow-400";
|
||||
} else if (sig.signed && !sig.valid) {
|
||||
statusText = "This plugin has an invalid signature and may have been tampered with.";
|
||||
statusColor = "text-red-400";
|
||||
}
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "plugin-enable-confirm-modal";
|
||||
overlay.className = "fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm";
|
||||
overlay.innerHTML = `
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-2xl p-6 max-w-md w-full mx-4 shadow-2xl">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<i class="fa-solid fa-triangle-exclamation text-2xl ${statusColor}"></i>
|
||||
<h3 class="text-lg font-semibold text-slate-100">Enable Unverified Plugin</h3>
|
||||
</div>
|
||||
<p class="text-sm text-slate-300 mb-2">${statusText}</p>
|
||||
<p class="text-sm text-slate-400 mb-1">Plugin: <strong class="text-slate-200">${plugin.name || plugin.id}</strong></p>
|
||||
${sig.fingerprint ? `<p class="text-xs text-slate-500 font-mono mb-3">Signer: ${sig.fingerprint}</p>` : '<p class="text-xs text-slate-500 mb-3">No signature present.</p>'}
|
||||
<p class="text-sm text-slate-400" style="margin-bottom: 16px;">Type <strong class="text-emerald-300">confirm</strong> below to enable this plugin:</p>
|
||||
<input type="text" id="plugin-enable-confirm-input" class="w-full px-3 py-2 rounded-lg bg-slate-800 border border-slate-600 text-slate-100 text-sm focus:outline-none focus:border-emerald-500" style="margin-bottom: 16px;" placeholder="Type confirm" autocomplete="off" spellcheck="false" />
|
||||
<div class="flex justify-end gap-2">
|
||||
<button id="plugin-enable-confirm-cancel" class="px-4 py-2 rounded-lg bg-slate-800 border border-slate-600 text-slate-300 hover:bg-slate-700">Cancel</button>
|
||||
<button id="plugin-enable-confirm-ok" disabled class="px-4 py-2 rounded-lg bg-emerald-900/40 border border-emerald-700/60 text-emerald-100 opacity-50 cursor-not-allowed">Enable Anyway</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const input = document.getElementById("plugin-enable-confirm-input");
|
||||
const okBtn = document.getElementById("plugin-enable-confirm-ok");
|
||||
const cancelBtn = document.getElementById("plugin-enable-confirm-cancel");
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const match = input.value.trim().toLowerCase() === "confirm";
|
||||
okBtn.disabled = !match;
|
||||
okBtn.classList.toggle("opacity-50", !match);
|
||||
okBtn.classList.toggle("cursor-not-allowed", !match);
|
||||
okBtn.classList.toggle("hover:bg-emerald-800/60", match);
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener("click", () => overlay.remove());
|
||||
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
|
||||
|
||||
okBtn.addEventListener("click", async () => {
|
||||
okBtn.disabled = true;
|
||||
okBtn.textContent = "Enabling...";
|
||||
try {
|
||||
const res = await fetch(`/api/plugins/${plugin.id}/enable`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled: true, confirmed: true }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
setStatus(`Enable failed: ${text}`, true);
|
||||
}
|
||||
} catch {
|
||||
setStatus("Enable failed", true);
|
||||
}
|
||||
overlay.remove();
|
||||
await refresh();
|
||||
});
|
||||
|
||||
input.focus();
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const plugins = await fetchPlugins();
|
||||
renderPlugins(plugins);
|
||||
@@ -237,5 +358,99 @@ fileInput?.addEventListener("change", (e) => {
|
||||
|
||||
refreshBtn?.addEventListener("click", refresh);
|
||||
|
||||
const trustedKeysSection = document.getElementById("trusted-keys-section");
|
||||
const trustedKeysList = document.getElementById("trusted-keys-list");
|
||||
const addKeyBtn = document.getElementById("add-trusted-key-btn");
|
||||
const newKeyInput = document.getElementById("new-trusted-key-input");
|
||||
|
||||
let _builtinKeys = [];
|
||||
|
||||
async function fetchTrustedKeys() {
|
||||
try {
|
||||
const res = await fetch("/api/plugins/trusted-keys");
|
||||
if (!res.ok) {
|
||||
if (res.status === 403) {
|
||||
if (trustedKeysSection) trustedKeysSection.classList.add("hidden");
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
const data = await res.json();
|
||||
if (trustedKeysSection) trustedKeysSection.classList.remove("hidden");
|
||||
_builtinKeys = Array.isArray(data.builtinKeys) ? data.builtinKeys : [];
|
||||
return Array.isArray(data.trustedKeys) ? data.trustedKeys : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function renderTrustedKeys(keys) {
|
||||
if (!trustedKeysList) return;
|
||||
trustedKeysList.innerHTML = "";
|
||||
if (!keys.length) {
|
||||
trustedKeysList.innerHTML = '<div class="text-slate-500 text-sm">No trusted keys configured. All plugins will require confirmation to load.</div>';
|
||||
return;
|
||||
}
|
||||
for (const key of keys) {
|
||||
const isBuiltin = _builtinKeys.includes(key);
|
||||
const row = document.createElement("div");
|
||||
row.className = "flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-800 bg-slate-950/50";
|
||||
const fp = document.createElement("span");
|
||||
fp.className = "font-mono text-sm text-slate-300 flex-1 truncate";
|
||||
fp.textContent = key;
|
||||
fp.title = key;
|
||||
row.appendChild(fp);
|
||||
if (isBuiltin) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "text-xs text-emerald-400 px-2 py-0.5 rounded bg-emerald-900/30 whitespace-nowrap";
|
||||
badge.textContent = "built-in";
|
||||
badge.title = "This key is hardcoded and always trusted";
|
||||
row.appendChild(badge);
|
||||
} else {
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.className = "text-red-400 hover:text-red-300 text-sm px-2 py-1";
|
||||
removeBtn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
||||
removeBtn.title = "Remove trusted key";
|
||||
removeBtn.addEventListener("click", async () => {
|
||||
const res = await fetch(`/api/plugins/trusted-keys/${key}`, { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
await refreshTrustedKeys();
|
||||
await refresh();
|
||||
}
|
||||
});
|
||||
row.appendChild(removeBtn);
|
||||
}
|
||||
trustedKeysList.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTrustedKeys() {
|
||||
const keys = await fetchTrustedKeys();
|
||||
renderTrustedKeys(keys);
|
||||
}
|
||||
|
||||
addKeyBtn?.addEventListener("click", async () => {
|
||||
const fp = newKeyInput?.value?.trim().toLowerCase();
|
||||
if (!fp || !/^[a-f0-9]{64}$/.test(fp)) {
|
||||
setStatus("Invalid fingerprint — must be a 64-character hex string (SHA-256)", true);
|
||||
return;
|
||||
}
|
||||
const res = await fetch("/api/plugins/trusted-keys", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ fingerprint: fp }),
|
||||
});
|
||||
if (res.ok) {
|
||||
if (newKeyInput) newKeyInput.value = "";
|
||||
setStatus("Trusted key added.");
|
||||
await refreshTrustedKeys();
|
||||
await refresh();
|
||||
} else {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
setStatus(`Failed to add key: ${data.error || "unknown error"}`, true);
|
||||
}
|
||||
});
|
||||
|
||||
checkAuth();
|
||||
refresh();
|
||||
refreshTrustedKeys();
|
||||
|
||||
Reference in New Issue
Block a user