encrypted logs, more customization for admins.
This commit is contained in:
@@ -1783,6 +1783,100 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function renderClientLogs(logs) {
|
||||
if (!Array.isArray(logs) || logs.length === 0) {
|
||||
return '<div class="text-sm text-slate-500 p-3">No secure logs returned.</div>';
|
||||
}
|
||||
return logs.map((entry) => {
|
||||
const ts = entry.at ? new Date(entry.at).toLocaleString() : "";
|
||||
const source = entry.source || "log";
|
||||
return `<div class="border-b border-slate-800 py-2">
|
||||
<div class="text-xs text-slate-500 mb-1">#${escapeHtml(entry.seq)} ${escapeHtml(source)} ${escapeHtml(ts)}</div>
|
||||
<pre class="whitespace-pre-wrap text-xs text-slate-200 font-mono">${escapeHtml(entry.text || "")}</pre>
|
||||
</div>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function openSecureLogsModal(clientId) {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "fixed inset-0 z-[10000] flex items-center justify-center bg-black/60";
|
||||
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "bg-slate-900 border border-slate-700 rounded-xl shadow-2xl p-5 w-[760px] max-w-[calc(100vw-24px)] max-h-[86vh] flex flex-col gap-4";
|
||||
modal.innerHTML = `
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-lg font-semibold text-slate-100 flex items-center gap-2"><i class="fa-solid fa-file-shield text-sky-400"></i> Secure Logs</h3>
|
||||
<button class="secure-logs-close text-slate-400 hover:text-white"><i class="fa-solid fa-xmark"></i></button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="secure-logs-fetch px-3 py-2 rounded-lg bg-sky-700 hover:bg-sky-600 text-white text-sm"><i class="fa-solid fa-download mr-1"></i> Request From Client</button>
|
||||
<button class="secure-logs-decrypt px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 text-sm"><i class="fa-solid fa-key mr-1"></i> Decrypt Pasted Blob</button>
|
||||
</div>
|
||||
<textarea class="secure-logs-input min-h-28 bg-slate-950 border border-slate-700 rounded-lg p-3 text-xs text-slate-200 font-mono outline-none focus:border-sky-500" placeholder="Paste OVERLORD-SECURE-LOG lines here for offline recovery"></textarea>
|
||||
<div class="secure-logs-meta text-xs text-slate-500"></div>
|
||||
<div class="secure-logs-output overflow-auto rounded-lg border border-slate-800 bg-slate-950/70 p-3 min-h-48"></div>
|
||||
`;
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const output = modal.querySelector(".secure-logs-output");
|
||||
const meta = modal.querySelector(".secure-logs-meta");
|
||||
const input = modal.querySelector(".secure-logs-input");
|
||||
const fetchBtn = modal.querySelector(".secure-logs-fetch");
|
||||
const decryptBtn = modal.querySelector(".secure-logs-decrypt");
|
||||
modal.querySelector(".secure-logs-close")?.addEventListener("click", () => overlay.remove());
|
||||
|
||||
fetchBtn.addEventListener("click", async () => {
|
||||
fetchBtn.disabled = true;
|
||||
output.innerHTML = '<div class="text-sm text-slate-400 p-3">Requesting logs...</div>';
|
||||
try {
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/logs`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ limit: 200 }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !data.ok) {
|
||||
meta.textContent = data.clientError || data.error || "Secure logs request failed";
|
||||
} else {
|
||||
meta.textContent = `Entries ${data.fromSeq || 0}-${data.toSeq || 0}; dropped ${data.dropped || 0}`;
|
||||
}
|
||||
output.innerHTML = renderClientLogs(data.logs || []);
|
||||
} catch (err) {
|
||||
meta.textContent = err.message || "Secure logs request failed";
|
||||
output.innerHTML = "";
|
||||
} finally {
|
||||
fetchBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
decryptBtn.addEventListener("click", async () => {
|
||||
const blob = input.value.trim();
|
||||
if (!blob) {
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
decryptBtn.disabled = true;
|
||||
output.innerHTML = '<div class="text-sm text-slate-400 p-3">Decrypting...</div>';
|
||||
try {
|
||||
const res = await fetch("/api/client-logs/decrypt", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ blob }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
meta.textContent = data.errors?.length ? `${data.errors.length} blob(s) could not decrypt` : "Offline blob decrypted";
|
||||
output.innerHTML = renderClientLogs(data.logs || []);
|
||||
} catch (err) {
|
||||
meta.textContent = err.message || "Decrypt failed";
|
||||
output.innerHTML = "";
|
||||
} finally {
|
||||
decryptBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.banClient = async (clientId) => {
|
||||
if (!clientId) return;
|
||||
if (!confirm(`Ban IP for ${clientId} and block future connections?`)) return;
|
||||
@@ -2054,6 +2148,11 @@ menu.addEventListener("click", async (e) => {
|
||||
if (ok) setTimeout(() => loadWithOptions({ force: true }), 200);
|
||||
closeMenu(clearContext);
|
||||
return;
|
||||
} else if (action === "secure-logs") {
|
||||
const savedClientId = contextCard;
|
||||
closeMenu(clearContext);
|
||||
openSecureLogsModal(savedClientId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isClientOnline(contextCard)) {
|
||||
|
||||
@@ -46,6 +46,7 @@ const MENU_GROUPS = [
|
||||
{ label: "Set Custom Tag", icon: "fa-solid fa-tag", icolor: "text-slate-300", action: "set-custom-tag" },
|
||||
{ label: "Set Group", icon: "fa-solid fa-layer-group", icolor: "text-blue-300", action: "set-group" },
|
||||
{ label: "Mute Notifications", icon: "fa-solid fa-bell-slash", icolor: "text-amber-300", action: "toggle-mute" },
|
||||
{ label: "Secure Logs", icon: "fa-solid fa-file-shield", icolor: "text-sky-300", action: "secure-logs" },
|
||||
{ divider: true },
|
||||
{ label: "Elevate", icon: "fa-solid fa-arrow-up-right-dots", icolor: "text-green-400", action: "elevate" },
|
||||
{ divider: true },
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
let currentUser = null;
|
||||
let users = [];
|
||||
let onlyOwnUsers = localStorage.getItem("adminOnlyOwnUsers") === "1";
|
||||
|
||||
import { escapeHtml, formatDate } from "./format.js";
|
||||
|
||||
const usersTableBody = document.getElementById("users-table-body");
|
||||
const addUserBtn = document.getElementById("add-user-btn");
|
||||
const ownUsersToggle = document.getElementById("own-users-toggle");
|
||||
const userModal = document.getElementById("user-modal");
|
||||
const modalTitle = document.getElementById("modal-title");
|
||||
const userForm = document.getElementById("user-form");
|
||||
@@ -78,6 +80,8 @@ async function getCurrentUser() {
|
||||
alert("Access denied. Admin role required.");
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
renderUsers();
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
@@ -130,19 +134,24 @@ async function loadUsers() {
|
||||
}
|
||||
|
||||
function renderUsers() {
|
||||
if (users.length === 0) {
|
||||
const visibleUsers = getVisibleUsers();
|
||||
|
||||
if (visibleUsers.length === 0) {
|
||||
const emptyMessage = onlyOwnUsers && users.length > 0
|
||||
? "No users created by you"
|
||||
: "No users found";
|
||||
usersTableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center text-slate-400">
|
||||
<i class="fa-solid fa-users mr-2"></i>
|
||||
No users found
|
||||
${emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
usersTableBody.innerHTML = users
|
||||
usersTableBody.innerHTML = visibleUsers
|
||||
.map(
|
||||
(user) => `
|
||||
<tr class="hover:bg-slate-800/30 transition-colors">
|
||||
@@ -275,6 +284,15 @@ function renderUsers() {
|
||||
attachActionListeners();
|
||||
}
|
||||
|
||||
function getVisibleUsers() {
|
||||
if (!onlyOwnUsers || !currentUser) return users;
|
||||
return users.filter((user) => (
|
||||
user.id === currentUser.userId ||
|
||||
user.username === currentUser.username ||
|
||||
user.created_by === currentUser.username
|
||||
));
|
||||
}
|
||||
|
||||
function getRoleBadge(role) {
|
||||
const badges = {
|
||||
admin:
|
||||
@@ -378,6 +396,15 @@ addUserBtn.addEventListener("click", () => {
|
||||
closeModal.addEventListener("click", hideModal);
|
||||
cancelBtn.addEventListener("click", hideModal);
|
||||
|
||||
if (ownUsersToggle) {
|
||||
ownUsersToggle.checked = onlyOwnUsers;
|
||||
ownUsersToggle.addEventListener("change", () => {
|
||||
onlyOwnUsers = ownUsersToggle.checked;
|
||||
localStorage.setItem("adminOnlyOwnUsers", onlyOwnUsers ? "1" : "0");
|
||||
renderUsers();
|
||||
});
|
||||
}
|
||||
|
||||
userForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
@@ -447,13 +447,13 @@
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-rose-500 transition-colors">
|
||||
<label class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-sky-500 transition-colors">
|
||||
<input type="checkbox" name="no-printing" class="w-4 h-4" />
|
||||
<div class="flex flex-col">
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-volume-xmark text-rose-400"></i> Disable Client Printing
|
||||
<i class="fa-solid fa-lock text-sky-400"></i> Secure Client Logs
|
||||
</span>
|
||||
<span class="text-xs text-slate-500">Silence all stdout/stderr and log output</span>
|
||||
<span class="text-xs text-slate-500">Capture stdout/stderr and logs as encrypted base64</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
@@ -47,12 +47,22 @@
|
||||
<h2 class="text-2xl font-bold text-slate-100 mb-1">Users</h2>
|
||||
<p class="text-slate-400 text-sm">Manage user accounts and roles</p>
|
||||
</div>
|
||||
<button
|
||||
id="add-user-btn"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
<i class="fa-solid fa-user-plus mr-2"></i>Add User
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="inline-flex items-center gap-2 px-3 py-2 bg-slate-900/60 border border-slate-800 rounded-lg text-sm text-slate-300">
|
||||
<input
|
||||
id="own-users-toggle"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-slate-600 bg-slate-800 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span>Only my users</span>
|
||||
</label>
|
||||
<button
|
||||
id="add-user-btn"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
<i class="fa-solid fa-user-plus mr-2"></i>Add User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
|
||||
Reference in New Issue
Block a user