fixed remove from dashboard + added nickname
This commit is contained in:
@@ -65,6 +65,19 @@ function detectClientPlatform(clientId) {
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getClientCard(clientId) {
|
||||||
|
if (!clientId) return null;
|
||||||
|
const selectorId =
|
||||||
|
typeof CSS !== "undefined" && typeof CSS.escape === "function"
|
||||||
|
? CSS.escape(clientId)
|
||||||
|
: clientId;
|
||||||
|
return document.querySelector(`article[data-id="${selectorId}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClientOnline(clientId) {
|
||||||
|
return getClientCard(clientId)?.dataset.online === "true";
|
||||||
|
}
|
||||||
|
|
||||||
function applyMenuSupportRules(clientId) {
|
function applyMenuSupportRules(clientId) {
|
||||||
const platform = detectClientPlatform(clientId);
|
const platform = detectClientPlatform(clientId);
|
||||||
const isWindows = platform === "windows";
|
const isWindows = platform === "windows";
|
||||||
@@ -240,7 +253,7 @@ function initializeRenderer() {
|
|||||||
pageLabel,
|
pageLabel,
|
||||||
openMenu: (id, x, y) => {
|
openMenu: (id, x, y) => {
|
||||||
applyMenuSupportRules(id);
|
applyMenuSupportRules(id);
|
||||||
openMenu(id, x, y, setContext);
|
openMenu(id, x, y, setContext, { isOnline: isClientOnline(id) });
|
||||||
loadPluginsForClient(id);
|
loadPluginsForClient(id);
|
||||||
},
|
},
|
||||||
openModal,
|
openModal,
|
||||||
@@ -510,6 +523,27 @@ window.removeClientFromDashboard = async (clientId) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.setClientNickname = async (clientId, nickname) => {
|
||||||
|
if (!clientId) return false;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/clients/${clientId}/nickname`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ nickname: nickname || "" }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
alert(data.error || "Failed to update client nickname");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert("Failed to update client nickname");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.banClient = async (clientId) => {
|
window.banClient = async (clientId) => {
|
||||||
if (!clientId) return;
|
if (!clientId) return;
|
||||||
if (!confirm(`Ban IP for ${clientId} and block future connections?`)) return;
|
if (!confirm(`Ban IP for ${clientId} and block future connections?`)) return;
|
||||||
@@ -686,6 +720,31 @@ menu.addEventListener("click", async (e) => {
|
|||||||
}
|
}
|
||||||
closeMenu(clearContext);
|
closeMenu(clearContext);
|
||||||
return;
|
return;
|
||||||
|
} else if (action === "set-nickname") {
|
||||||
|
const card = getClientCard(contextCard);
|
||||||
|
const currentNickname = (card?.dataset.nickname || "").trim();
|
||||||
|
const input = prompt(
|
||||||
|
`Set nickname for ${contextCard}\n\nLeave blank to clear nickname.`,
|
||||||
|
currentNickname,
|
||||||
|
);
|
||||||
|
if (input === null) {
|
||||||
|
closeMenu(clearContext);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = input.trim();
|
||||||
|
const updated = await window.setClientNickname(contextCard, trimmed || null);
|
||||||
|
if (updated) {
|
||||||
|
setTimeout(() => loadWithOptions({ force: true }), 200);
|
||||||
|
}
|
||||||
|
closeMenu(clearContext);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isClientOnline(contextCard)) {
|
||||||
|
alert("Client is offline. This command can only be used while the client is online.");
|
||||||
|
closeMenu(clearContext);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ok = await sendCommand(contextCard, action);
|
const ok = await sendCommand(contextCard, action);
|
||||||
|
|||||||
@@ -132,8 +132,6 @@ export function createRenderer({
|
|||||||
card.addEventListener("contextmenu", (e) => {
|
card.addEventListener("contextmenu", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (isViewer) return;
|
if (isViewer) return;
|
||||||
const isOnline = card.dataset.online === "true";
|
|
||||||
if (!isOnline) return;
|
|
||||||
const clientId = card.dataset.id;
|
const clientId = card.dataset.id;
|
||||||
if (!clientId) return;
|
if (!clientId) return;
|
||||||
const { clientX, clientY } = e;
|
const { clientX, clientY } = e;
|
||||||
@@ -167,12 +165,15 @@ export function createRenderer({
|
|||||||
|
|
||||||
card.dataset.online = String(!!client.online);
|
card.dataset.online = String(!!client.online);
|
||||||
card.dataset.os = String(client.os || "").toLowerCase();
|
card.dataset.os = String(client.os || "").toLowerCase();
|
||||||
|
card.dataset.nickname = String(client.nickname || "");
|
||||||
const os = osBadge(client.os || "unknown");
|
const os = osBadge(client.os || "unknown");
|
||||||
const arch = archBadge(client.arch || "");
|
const arch = archBadge(client.arch || "");
|
||||||
const ver = versionBadge(client.version || "");
|
const ver = versionBadge(client.version || "");
|
||||||
const mons = monitorsBadge(client.monitors);
|
const mons = monitorsBadge(client.monitors);
|
||||||
const deviceId = shortId(client.id);
|
const deviceId = shortId(client.id);
|
||||||
const hwid = shortId(client.hwid || "");
|
const hwid = shortId(client.hwid || "");
|
||||||
|
const nickname = String(client.nickname || "").trim();
|
||||||
|
const displayName = nickname || client.host || deviceId;
|
||||||
card.className = `card rounded-xl border border-slate-800 bg-slate-900/70 p-4 shadow-lg ${client.online ? "" : "card-offline"} tone-${os.tone}`;
|
card.className = `card rounded-xl border border-slate-800 bg-slate-900/70 p-4 shadow-lg ${client.online ? "" : "card-offline"} tone-${os.tone}`;
|
||||||
const cardThumb = client.thumbnail
|
const cardThumb = client.thumbnail
|
||||||
? (() => {
|
? (() => {
|
||||||
@@ -202,7 +203,8 @@ export function createRenderer({
|
|||||||
<div class="flex-1 min-w-[240px] flex flex-col gap-2">
|
<div class="flex-1 min-w-[240px] flex flex-col gap-2">
|
||||||
<div class="flex items-center gap-3 flex-wrap text-lg font-semibold">
|
<div class="flex items-center gap-3 flex-wrap text-lg font-semibold">
|
||||||
<span class="text-2xl">${countryToFlag(client.country)}</span>
|
<span class="text-2xl">${countryToFlag(client.country)}</span>
|
||||||
<span>${escapeHtml(client.host || deviceId)}</span>
|
<span>${escapeHtml(displayName)}</span>
|
||||||
|
${nickname && client.host ? `<span class="pill pill-ghost text-xs"><i class="fa-solid fa-laptop"></i> ${escapeHtml(client.host)}</span>` : ""}
|
||||||
<span class="text-slate-300 text-lg font-semibold flex items-center gap-1"><i class="fa-solid fa-user"></i> ${escapeHtml(client.user || "unknown")}</span>
|
<span class="text-slate-300 text-lg font-semibold flex items-center gap-1"><i class="fa-solid fa-user"></i> ${escapeHtml(client.user || "unknown")}</span>
|
||||||
<span class="pill ${client.online ? "pill-online" : "pill-offline"}">
|
<span class="pill ${client.online ? "pill-online" : "pill-offline"}">
|
||||||
<i class="fa-solid fa-circle"></i>
|
<i class="fa-solid fa-circle"></i>
|
||||||
@@ -223,7 +225,7 @@ export function createRenderer({
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="text-emerald-300 font-mono text-sm inline-flex items-center gap-2"><i class="fa-solid fa-satellite-dish"></i> ${formatPing(client.pingMs)}</span>
|
<span class="text-emerald-300 font-mono text-sm inline-flex items-center gap-2"><i class="fa-solid fa-satellite-dish"></i> ${formatPing(client.pingMs)}</span>
|
||||||
${isViewer ? "" : `<button ${client.online ? "" : "disabled"} class="command-btn inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-800 bg-slate-800/70 hover:bg-slate-700 disabled:opacity-50" data-id="${escapeHtml(client.id)}"><i class="fa-solid fa-bars"></i> Commands</button>`}
|
${isViewer ? "" : `<button class="command-btn inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-800 bg-slate-800/70 hover:bg-slate-700" data-id="${escapeHtml(client.id)}"><i class="fa-solid fa-bars"></i> Commands</button>`}
|
||||||
${isViewer ? "" : `<button class="ban-btn inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-red-800 bg-red-900/60 hover:bg-red-800 text-red-100" data-id="${escapeHtml(client.id)}"><i class="fa-solid fa-ban"></i> Ban</button>`}
|
${isViewer ? "" : `<button class="ban-btn inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-red-800 bg-red-900/60 hover:bg-red-800 text-red-100" data-id="${escapeHtml(client.id)}"><i class="fa-solid fa-ban"></i> Ban</button>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ menu.innerHTML = `
|
|||||||
<button class="w-full text-left px-3 py-2.5 mb-1 rounded-lg border border-slate-800 bg-slate-800/60 hover:bg-slate-700 flex items-center gap-2" data-action="reconnect"><i class="fa-solid fa-rotate"></i> Reconnect</button>
|
<button class="w-full text-left px-3 py-2.5 mb-1 rounded-lg border border-slate-800 bg-slate-800/60 hover:bg-slate-700 flex items-center gap-2" data-action="reconnect"><i class="fa-solid fa-rotate"></i> Reconnect</button>
|
||||||
<button class="w-full text-left px-3 py-2.5 mb-1 rounded-lg border border-red-800 bg-red-900/40 hover:bg-red-800/60 text-red-100 flex items-center gap-2" data-action="disconnect"><i class="fa-solid fa-plug-circle-xmark"></i> Disconnect</button>
|
<button class="w-full text-left px-3 py-2.5 mb-1 rounded-lg border border-red-800 bg-red-900/40 hover:bg-red-800/60 text-red-100 flex items-center gap-2" data-action="disconnect"><i class="fa-solid fa-plug-circle-xmark"></i> Disconnect</button>
|
||||||
<button class="w-full text-left px-3 py-2.5 rounded-lg border border-red-900 bg-red-950/60 hover:bg-red-900/80 text-red-200 flex items-center gap-2" data-action="uninstall"><i class="fa-solid fa-trash"></i> Uninstall</button>
|
<button class="w-full text-left px-3 py-2.5 rounded-lg border border-red-900 bg-red-950/60 hover:bg-red-900/80 text-red-200 flex items-center gap-2" data-action="uninstall"><i class="fa-solid fa-trash"></i> Uninstall</button>
|
||||||
<button class="w-full mt-1 text-left px-3 py-2.5 rounded-lg border border-rose-900 bg-rose-950/70 hover:bg-rose-900/90 text-rose-200 flex items-center gap-2" data-action="remove-dashboard"><i class="fa-solid fa-user-xmark"></i> Remove From Dashboard</button>
|
<button class="w-full mt-1 text-left px-3 py-2.5 rounded-lg border border-slate-700 bg-slate-800/60 hover:bg-slate-700 text-slate-100 flex items-center gap-2" data-action="set-nickname"><i class="fa-solid fa-signature"></i> Set Nickname</button>
|
||||||
|
<button class="hidden w-full mt-1 text-left px-3 py-2.5 rounded-lg border border-rose-900 bg-rose-950/70 hover:bg-rose-900/90 text-rose-200 flex items-center gap-2" data-action="remove-dashboard"><i class="fa-solid fa-user-xmark"></i> Remove From Dashboard</button>
|
||||||
<div id="plugin-section" class="hidden">
|
<div id="plugin-section" class="hidden">
|
||||||
<div class="border-t border-slate-700 my-2"></div>
|
<div class="border-t border-slate-700 my-2"></div>
|
||||||
<div class="text-[11px] uppercase tracking-wider text-slate-400 px-1 mb-1">Plugins</div>
|
<div class="text-[11px] uppercase tracking-wider text-slate-400 px-1 mb-1">Plugins</div>
|
||||||
@@ -35,8 +36,15 @@ modal.innerHTML = `<div class="max-w-5xl max-h-[90vh] p-4"><img class="max-h-[85
|
|||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
const modalImg = modal.querySelector("#modal-img");
|
const modalImg = modal.querySelector("#modal-img");
|
||||||
|
|
||||||
export function openMenu(clientId, x, y, setContext) {
|
export function openMenu(clientId, x, y, setContext, options = {}) {
|
||||||
if (setContext) setContext(clientId);
|
if (setContext) setContext(clientId);
|
||||||
|
|
||||||
|
const removeBtn = menu.querySelector('[data-action="remove-dashboard"]');
|
||||||
|
if (removeBtn) {
|
||||||
|
const isOnline = options.isOnline === true;
|
||||||
|
removeBtn.classList.toggle("hidden", isOnline);
|
||||||
|
}
|
||||||
|
|
||||||
menu.classList.remove("hidden");
|
menu.classList.remove("hidden");
|
||||||
|
|
||||||
const menuWidth = 192;
|
const menuWidth = 192;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
id="search"
|
id="search"
|
||||||
class="bg-transparent outline-none w-full placeholder:text-slate-500"
|
class="bg-transparent outline-none w-full placeholder:text-slate-500"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search by host, user, id"
|
placeholder="Search by nickname, host, user, id"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ db.run(`
|
|||||||
arch TEXT,
|
arch TEXT,
|
||||||
version TEXT,
|
version TEXT,
|
||||||
user TEXT,
|
user TEXT,
|
||||||
|
nickname TEXT,
|
||||||
monitors INTEGER,
|
monitors INTEGER,
|
||||||
country TEXT,
|
country TEXT,
|
||||||
last_seen INTEGER,
|
last_seen INTEGER,
|
||||||
@@ -36,6 +37,9 @@ try {
|
|||||||
try {
|
try {
|
||||||
db.run(`ALTER TABLE clients ADD COLUMN ip TEXT`);
|
db.run(`ALTER TABLE clients ADD COLUMN ip TEXT`);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
try {
|
||||||
|
db.run(`ALTER TABLE clients ADD COLUMN nickname TEXT`);
|
||||||
|
} catch {}
|
||||||
db.run(
|
db.run(
|
||||||
`CREATE INDEX IF NOT EXISTS idx_clients_online_last_seen ON clients(online, last_seen DESC);`,
|
`CREATE INDEX IF NOT EXISTS idx_clients_online_last_seen ON clients(online, last_seen DESC);`,
|
||||||
);
|
);
|
||||||
@@ -134,6 +138,7 @@ export function upsertClientRow(
|
|||||||
arch=COALESCE(excluded.arch, clients.arch),
|
arch=COALESCE(excluded.arch, clients.arch),
|
||||||
version=COALESCE(excluded.version, clients.version),
|
version=COALESCE(excluded.version, clients.version),
|
||||||
user=COALESCE(excluded.user, clients.user),
|
user=COALESCE(excluded.user, clients.user),
|
||||||
|
nickname=clients.nickname,
|
||||||
monitors=COALESCE(excluded.monitors, clients.monitors),
|
monitors=COALESCE(excluded.monitors, clients.monitors),
|
||||||
country=COALESCE(excluded.country, clients.country),
|
country=COALESCE(excluded.country, clients.country),
|
||||||
last_seen=excluded.last_seen,
|
last_seen=excluded.last_seen,
|
||||||
@@ -178,6 +183,26 @@ export function deleteClientRow(id: string) {
|
|||||||
db.run(`DELETE FROM clients WHERE id=?`, id);
|
db.run(`DELETE FROM clients WHERE id=?`, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getClientOnlineState(id: string): boolean | null {
|
||||||
|
const row = db.query<{ online: number }>(`SELECT online FROM clients WHERE id=?`).get(id);
|
||||||
|
if (!row) return null;
|
||||||
|
return row.online === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setClientNickname(id: string, nickname: string | null): boolean {
|
||||||
|
const result = db.run(
|
||||||
|
`UPDATE clients SET nickname=? WHERE id=?`,
|
||||||
|
nickname && nickname.trim() ? nickname.trim() : null,
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
return ((result as any)?.changes || 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClientNickname(id: string): string | null {
|
||||||
|
const row = db.query<{ nickname: string | null }>(`SELECT nickname FROM clients WHERE id=?`).get(id);
|
||||||
|
return row?.nickname ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export function getClientIp(id: string): string | null {
|
export function getClientIp(id: string): string | null {
|
||||||
const row = db.query<{ ip: string }>(`SELECT ip FROM clients WHERE id=?`).get(id);
|
const row = db.query<{ ip: string }>(`SELECT ip FROM clients WHERE id=?`).get(id);
|
||||||
return row?.ip || null;
|
return row?.ip || null;
|
||||||
@@ -242,10 +267,10 @@ export function listClients(filters: ListFilters): ListResult {
|
|||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
where.push(
|
where.push(
|
||||||
"(LOWER(COALESCE(host,'')) LIKE ? OR LOWER(COALESCE(user,'')) LIKE ? OR LOWER(id) LIKE ?)",
|
"(LOWER(COALESCE(host,'')) LIKE ? OR LOWER(COALESCE(user,'')) LIKE ? OR LOWER(COALESCE(nickname,'')) LIKE ? OR LOWER(id) LIKE ?)",
|
||||||
);
|
);
|
||||||
const needle = `%${search}%`;
|
const needle = `%${search}%`;
|
||||||
params.push(needle, needle, needle);
|
params.push(needle, needle, needle, needle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusFilter === "online") {
|
if (statusFilter === "online") {
|
||||||
@@ -282,9 +307,9 @@ export function listClients(filters: ListFilters): ListResult {
|
|||||||
case "ping_desc":
|
case "ping_desc":
|
||||||
return "ORDER BY ping_ms IS NULL, ping_ms DESC";
|
return "ORDER BY ping_ms IS NULL, ping_ms DESC";
|
||||||
case "host_asc":
|
case "host_asc":
|
||||||
return "ORDER BY LOWER(host) ASC";
|
return "ORDER BY LOWER(COALESCE(nickname, host)) ASC";
|
||||||
case "host_desc":
|
case "host_desc":
|
||||||
return "ORDER BY LOWER(host) DESC";
|
return "ORDER BY LOWER(COALESCE(nickname, host)) DESC";
|
||||||
default:
|
default:
|
||||||
return "ORDER BY last_seen DESC";
|
return "ORDER BY last_seen DESC";
|
||||||
}
|
}
|
||||||
@@ -302,7 +327,7 @@ export function listClients(filters: ListFilters): ListResult {
|
|||||||
|
|
||||||
const rows = db
|
const rows = db
|
||||||
.query<any>(
|
.query<any>(
|
||||||
`SELECT id, hwid, role, host, os, arch, version, user, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs
|
`SELECT id, hwid, role, host, os, arch, version, user, nickname, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs
|
||||||
FROM clients
|
FROM clients
|
||||||
${whereSql}
|
${whereSql}
|
||||||
${orderBy}
|
${orderBy}
|
||||||
@@ -320,6 +345,7 @@ export function listClients(filters: ListFilters): ListResult {
|
|||||||
arch: c.arch || "arch?",
|
arch: c.arch || "arch?",
|
||||||
version: c.version || "0",
|
version: c.version || "0",
|
||||||
user: c.user,
|
user: c.user,
|
||||||
|
nickname: c.nickname || null,
|
||||||
monitors: c.monitors,
|
monitors: c.monitors,
|
||||||
country: c.country || "ZZ",
|
country: c.country || "ZZ",
|
||||||
pingMs: c.pingMs ?? null,
|
pingMs: c.pingMs ?? null,
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import {
|
|||||||
banIp,
|
banIp,
|
||||||
clientExists,
|
clientExists,
|
||||||
deleteClientRow,
|
deleteClientRow,
|
||||||
|
getClientOnlineState,
|
||||||
getClientIp,
|
getClientIp,
|
||||||
isIpBanned,
|
isIpBanned,
|
||||||
listBannedIps,
|
listBannedIps,
|
||||||
listClients,
|
listClients,
|
||||||
|
setClientNickname,
|
||||||
setOnlineState,
|
setOnlineState,
|
||||||
unbanIp,
|
unbanIp,
|
||||||
} from "../../db";
|
} from "../../db";
|
||||||
@@ -238,6 +240,13 @@ export async function handleClientRoutes(
|
|||||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||||
}
|
}
|
||||||
const target = clientManager.getClient(targetId);
|
const target = clientManager.getClient(targetId);
|
||||||
|
const isOnlineInDb = getClientOnlineState(targetId);
|
||||||
|
if (target?.online || isOnlineInDb === true) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Client is online. Remove from dashboard is only allowed for offline clients." },
|
||||||
|
{ status: 409 },
|
||||||
|
);
|
||||||
|
}
|
||||||
const existsInDb = clientExists(targetId);
|
const existsInDb = clientExists(targetId);
|
||||||
if (!target && !existsInDb) {
|
if (!target && !existsInDb) {
|
||||||
return Response.json({ error: "Client not found" }, { status: 404 });
|
return Response.json({ error: "Client not found" }, { status: 404 });
|
||||||
@@ -269,6 +278,62 @@ export async function handleClientRoutes(
|
|||||||
return Response.json({ ok: true }, { headers: deps.CORS_HEADERS });
|
return Response.json({ ok: true }, { headers: deps.CORS_HEADERS });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clientNicknameMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/nickname$/);
|
||||||
|
if (req.method === "PATCH" && clientNicknameMatch) {
|
||||||
|
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 = clientNicknameMatch[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 rawNickname = typeof body?.nickname === "string" ? body.nickname : "";
|
||||||
|
const trimmed = rawNickname.trim();
|
||||||
|
if (trimmed.length > 64) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Nickname must be 64 characters or fewer" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nickname = trimmed.length ? trimmed : null;
|
||||||
|
const updated = setClientNickname(targetId, nickname);
|
||||||
|
if (!updated) {
|
||||||
|
return Response.json({ error: "Client not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip = server.requestIP(req)?.address || "unknown";
|
||||||
|
logAudit({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
username: user.username,
|
||||||
|
ip,
|
||||||
|
action: AuditAction.COMMAND,
|
||||||
|
targetClientId: targetId,
|
||||||
|
details: nickname ? `set_nickname:${nickname}` : "clear_nickname",
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({ ok: true, nickname }, { headers: deps.CORS_HEADERS });
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
const cmdMatch = url.pathname.match(/^\/api\/clients\/(.+)\/command$/);
|
const cmdMatch = url.pathname.match(/^\/api\/clients\/(.+)\/command$/);
|
||||||
if (cmdMatch) {
|
if (cmdMatch) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type ClientInfo = {
|
|||||||
arch?: string;
|
arch?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
user?: string;
|
user?: string;
|
||||||
|
nickname?: string;
|
||||||
monitors?: number;
|
monitors?: number;
|
||||||
monitorInfo?: { width: number; height: number }[];
|
monitorInfo?: { width: number; height: number }[];
|
||||||
country?: string;
|
country?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user