better permissions mac
This commit is contained in:
@@ -2589,6 +2589,13 @@ tbody tr:hover {
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cv-perm-summary {
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.cv-perm-summary.is-ok { color: #86efac; }
|
||||
.cv-perm-summary.is-warn { color: #fcd34d; }
|
||||
.cv-perm-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -2599,7 +2606,11 @@ tbody tr:hover {
|
||||
border: 1px solid transparent;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
appearance: none;
|
||||
}
|
||||
.cv-perm-chip:hover { filter: brightness(1.08); }
|
||||
.cv-perm-chip.is-ok { background: rgba(16, 185, 129, 0.1); color: #86efac; border-color: rgba(16, 185, 129, 0.25); }
|
||||
.cv-perm-chip.is-missing { background: rgba(244, 63, 94, 0.1); color: #fda4af; border-color: rgba(244, 63, 94, 0.25); }
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ let contextCard = null;
|
||||
let availableOsList = new Set();
|
||||
let rendererInitialized = false;
|
||||
let lastKnownTotalPages = 1;
|
||||
let currentServerVersion = "";
|
||||
|
||||
window.setDashboardPageBounds = (currentPage, totalPages) => {
|
||||
lastKnownTotalPages = Math.max(1, Number(totalPages) || 1);
|
||||
@@ -114,8 +115,14 @@ async function loadServerVersion() {
|
||||
const version = typeof payload?.version === "string" && payload.version.trim()
|
||||
? payload.version.trim()
|
||||
: "unknown";
|
||||
currentServerVersion = version;
|
||||
setServerVersionLabel(version, version === "unknown" ? "warn" : "ok");
|
||||
if (rendererInitialized) {
|
||||
state.lastDigest = "";
|
||||
renderCachedClients({ force: true });
|
||||
}
|
||||
} catch {
|
||||
currentServerVersion = "unavailable";
|
||||
setServerVersionLabel("unavailable", "bad");
|
||||
}
|
||||
}
|
||||
@@ -173,6 +180,62 @@ function getClientCard(clientId) {
|
||||
return document.querySelector(`[data-client-row][data-id="${selectorId}"]`);
|
||||
}
|
||||
|
||||
function macPermissionLabels(keys = []) {
|
||||
const labels = {
|
||||
accessibility: "Accessibility",
|
||||
screenRecording: "Screen Recording",
|
||||
fullDiskAccess: "Full Disk Access",
|
||||
};
|
||||
return keys.map((key) => labels[key] || key).join(", ");
|
||||
}
|
||||
|
||||
async function requestMacPermissions(clientId, permissionKey = "") {
|
||||
if (!clientId) return;
|
||||
if (!isClientOnline(clientId)) {
|
||||
alert("Client is offline. macOS permissions can only be requested while the client is online.");
|
||||
return;
|
||||
}
|
||||
if (detectClientPlatform(clientId) !== "mac") {
|
||||
alert("This permission request is only available for macOS clients.");
|
||||
return;
|
||||
}
|
||||
|
||||
const requested = permissionKey ? [permissionKey] : ["accessibility", "screenRecording", "fullDiskAccess"];
|
||||
const label = macPermissionLabels(requested) || "macOS permissions";
|
||||
const proceed = confirm(
|
||||
`Request ${label} from ${clientId}?\n\n` +
|
||||
"This can show a macOS prompt on the target Mac. " +
|
||||
"Full Disk Access must be granted in System Settings; the agent will open the Privacy pane when possible.",
|
||||
);
|
||||
if (!proceed) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "darwin_request_permissions",
|
||||
permissions: requested,
|
||||
}),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
alert(data.error || "macOS permission request failed.");
|
||||
return;
|
||||
}
|
||||
const missing = Array.isArray(data.missing) ? data.missing : [];
|
||||
const stillMissingRequested = missing.filter((key) => requested.includes(key));
|
||||
if (stillMissingRequested.length) {
|
||||
alert(`Permission request completed, but still missing: ${macPermissionLabels(stillMissingRequested)}`);
|
||||
} else {
|
||||
alert(`${label} is granted.`);
|
||||
}
|
||||
setTimeout(() => loadWithOptions({ force: true }), 300);
|
||||
} catch (err) {
|
||||
alert("macOS permission request failed: " + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
window.__uninstallingClientIds = window.__uninstallingClientIds || new Set();
|
||||
|
||||
function flipFallAndRemove(clientId, { delayMs = 0 } = {}) {
|
||||
@@ -846,7 +909,9 @@ function initializeRenderer() {
|
||||
requestPreview,
|
||||
requestThumbnail,
|
||||
pingClient: (id) => sendCommand(id, "ping"),
|
||||
onMacPermissionRequest: (id, _card, permissionKey) => requestMacPermissions(id, permissionKey),
|
||||
userRole: currentUser?.role,
|
||||
getServerVersion: () => currentServerVersion,
|
||||
getDisplayFields: () => displayFields,
|
||||
});
|
||||
rendererSetLayout = rSetLayout;
|
||||
|
||||
@@ -232,12 +232,14 @@ function macPermissionBadgeHtml(client) {
|
||||
function macPermissionDetailHtml(client) {
|
||||
const state = macPermissionState(client);
|
||||
if (!state) return "";
|
||||
const ok = state.missing.length === 0;
|
||||
const summary = ok ? "All required permissions granted" : `Missing: ${state.missing.map((item) => item.label).join(", ")}`;
|
||||
const chips = state.items.map((item) => `
|
||||
<span class="cv-perm-chip ${item.granted ? "is-ok" : "is-missing"}">
|
||||
<button type="button" class="cv-perm-chip ${item.granted ? "is-ok" : "is-missing"}" data-mac-permission-key="${escapeHtml(item.key)}" title="Request ${escapeHtml(item.label)} permission">
|
||||
<i class="fa-solid ${item.granted ? "fa-check" : "fa-xmark"}"></i>${escapeHtml(item.label)}
|
||||
</span>
|
||||
</button>
|
||||
`).join("");
|
||||
return `<div class="cv-field cv-field-wide"><span class="cv-field-label">macOS Permissions</span><span class="cv-field-value cv-perm-list">${chips}</span></div>`;
|
||||
return `<div class="cv-field cv-field-wide cv-field-macperms"><span class="cv-field-label">macOS Permissions</span><span class="cv-field-value cv-perm-list"><span class="cv-perm-summary ${ok ? "is-ok" : "is-warn"}">${escapeHtml(summary)}</span>${chips}</span></div>`;
|
||||
}
|
||||
|
||||
function shortOsLabel(osRaw = "") {
|
||||
@@ -259,6 +261,17 @@ function shortOsLabel(osRaw = "") {
|
||||
return osRaw || "?";
|
||||
}
|
||||
|
||||
function normalizeVersion(version) {
|
||||
return String(version || "").trim().replace(/^v/i, "");
|
||||
}
|
||||
|
||||
function isClientVersionCurrent(clientVersion, serverVersion) {
|
||||
const client = normalizeVersion(clientVersion);
|
||||
const server = normalizeVersion(serverVersion);
|
||||
if (!client || !server || server === "unknown" || server === "unavailable") return true;
|
||||
return client === server;
|
||||
}
|
||||
|
||||
export function createRenderer({
|
||||
grid,
|
||||
totalPill,
|
||||
@@ -268,7 +281,9 @@ export function createRenderer({
|
||||
requestPreview,
|
||||
requestThumbnail,
|
||||
pingClient,
|
||||
onMacPermissionRequest,
|
||||
userRole,
|
||||
getServerVersion,
|
||||
getDisplayFields,
|
||||
}) {
|
||||
const isViewer = userRole === "viewer";
|
||||
@@ -367,6 +382,14 @@ export function createRenderer({
|
||||
return;
|
||||
}
|
||||
|
||||
const macPermBtn = e.target.closest(".cv-perm-chip[data-mac-permission-key]");
|
||||
if (macPermBtn) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onMacPermissionRequest) onMacPermissionRequest(clientId, card, macPermBtn.dataset.macPermissionKey);
|
||||
return;
|
||||
}
|
||||
|
||||
const copyBtn = e.target.closest(".copy-id-btn");
|
||||
if (copyBtn) {
|
||||
e.stopPropagation();
|
||||
@@ -803,7 +826,7 @@ export function createRenderer({
|
||||
const showBattery = showField("battery");
|
||||
const cpuHtml = client.cpu ? cpuBadgeHtml(client.cpu) : "—";
|
||||
const batteryIndicator = showBattery ? batteryHtml(client) : "";
|
||||
const verLatest = String(client.version || "").startsWith("2.0.");
|
||||
const verLatest = isClientVersionCurrent(client.version, typeof getServerVersion === "function" ? getServerVersion() : "");
|
||||
|
||||
const metaParts = [
|
||||
showField("system") ? `<span class="cv-os cv-tone-${os.tone}"><i class="fa ${os.icon}"></i> ${escapeHtml(shortOsLabel(client.os))}</span>` : "",
|
||||
@@ -956,7 +979,7 @@ export function createRenderer({
|
||||
const customTag = String(client.customTag || "").trim();
|
||||
const displayName = nickname || client.host || deviceId;
|
||||
const userLine = client.user || "unknown";
|
||||
const verLatest = String(client.version || "").startsWith("2.0.");
|
||||
const verLatest = isClientVersionCurrent(client.version, typeof getServerVersion === "function" ? getServerVersion() : "");
|
||||
const hasGroup = !!String(client.groupName || "").trim();
|
||||
const showHost = nickname && client.host && nickname !== client.host;
|
||||
const showHardware = showField("hardware");
|
||||
|
||||
@@ -119,6 +119,7 @@ export type CommandType =
|
||||
| "desktop_select_display"
|
||||
| "desktop_enable_mouse"
|
||||
| "desktop_enable_keyboard"
|
||||
| "darwin_request_permissions"
|
||||
| "webrtc_publish"
|
||||
| "webrtc_stop"
|
||||
| "webrtc_p2p_offer"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { authenticateRequest } from "../../auth";
|
||||
import { AuditAction, logAudit } from "../../auditLog";
|
||||
import * as clientManager from "../../clientManager";
|
||||
import { deleteClientRow } from "../../db";
|
||||
import { deleteClientRow, upsertClientRow } from "../../db";
|
||||
import { metrics } from "../../metrics";
|
||||
import { encodeMessage } from "../../protocol";
|
||||
import { requireClientAccess, requireFeatureAccess, requirePermission } from "../../rbac";
|
||||
@@ -106,6 +106,46 @@ export async function handleClientCommandRoute(
|
||||
} else if (action === "desktop_start") {
|
||||
target.ws.send(encodeMessage({ type: "command", commandType: "desktop_start", id: uuidv4() }));
|
||||
metrics.recordCommand("desktop_start");
|
||||
} else if (action === "darwin_request_permissions") {
|
||||
const targetOs = String(target.os || "").toLowerCase();
|
||||
if (!targetOs.includes("darwin") && !targetOs.includes("mac")) {
|
||||
return Response.json({ ok: false, error: "macOS permission requests are only available for macOS clients" }, { status: 400 });
|
||||
}
|
||||
const requested = Array.isArray(body?.permissions)
|
||||
? body.permissions.filter((p: unknown) => typeof p === "string").slice(0, 8)
|
||||
: [];
|
||||
const cmdId = uuidv4();
|
||||
const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
deps.pendingCommandReplies.delete(cmdId);
|
||||
reject(new Error("macOS permission request timed out"));
|
||||
}, 45_000);
|
||||
deps.pendingCommandReplies.set(cmdId, { resolve, reject, timeout, clientId: targetId });
|
||||
});
|
||||
|
||||
target.ws.send(encodeMessage({ type: "command", commandType: "darwin_request_permissions", id: cmdId, payload: { permissions: requested } } as any));
|
||||
metrics.recordCommand("darwin_request_permissions");
|
||||
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, targetClientId: targetId, success: true, details: "darwin_request_permissions" });
|
||||
|
||||
try {
|
||||
const result = await replyPromise;
|
||||
let detail: any = {};
|
||||
if (result.message) {
|
||||
try { detail = JSON.parse(result.message); } catch { detail = {}; }
|
||||
}
|
||||
if (detail.permissions && typeof detail.permissions === "object" && !Array.isArray(detail.permissions)) {
|
||||
target.permissions = detail.permissions;
|
||||
upsertClientRow({ id: targetId, permissions: target.permissions, lastSeen: target.lastSeen, online: target.online ? 1 : 0 });
|
||||
}
|
||||
return Response.json({
|
||||
ok: result.ok,
|
||||
permissions: detail.permissions || null,
|
||||
missing: Array.isArray(detail.missing) ? detail.missing : [],
|
||||
message: result.message || "",
|
||||
}, { headers: deps.CORS_HEADERS });
|
||||
} catch (error: any) {
|
||||
return Response.json({ ok: false, error: error.message || "macOS permission request failed" }, { status: 504 });
|
||||
}
|
||||
} else if (action === "script_exec") {
|
||||
const scriptContent = body?.script || "";
|
||||
const scriptType = body?.scriptType || "powershell";
|
||||
|
||||
Reference in New Issue
Block a user