fixed remove from dashboard + added nickname
This commit is contained in:
@@ -20,6 +20,7 @@ db.run(`
|
||||
arch TEXT,
|
||||
version TEXT,
|
||||
user TEXT,
|
||||
nickname TEXT,
|
||||
monitors INTEGER,
|
||||
country TEXT,
|
||||
last_seen INTEGER,
|
||||
@@ -36,6 +37,9 @@ try {
|
||||
try {
|
||||
db.run(`ALTER TABLE clients ADD COLUMN ip TEXT`);
|
||||
} catch {}
|
||||
try {
|
||||
db.run(`ALTER TABLE clients ADD COLUMN nickname TEXT`);
|
||||
} catch {}
|
||||
db.run(
|
||||
`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),
|
||||
version=COALESCE(excluded.version, clients.version),
|
||||
user=COALESCE(excluded.user, clients.user),
|
||||
nickname=clients.nickname,
|
||||
monitors=COALESCE(excluded.monitors, clients.monitors),
|
||||
country=COALESCE(excluded.country, clients.country),
|
||||
last_seen=excluded.last_seen,
|
||||
@@ -178,6 +183,26 @@ export function deleteClientRow(id: string) {
|
||||
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 {
|
||||
const row = db.query<{ ip: string }>(`SELECT ip FROM clients WHERE id=?`).get(id);
|
||||
return row?.ip || null;
|
||||
@@ -242,10 +267,10 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
|
||||
if (search) {
|
||||
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}%`;
|
||||
params.push(needle, needle, needle);
|
||||
params.push(needle, needle, needle, needle);
|
||||
}
|
||||
|
||||
if (statusFilter === "online") {
|
||||
@@ -282,9 +307,9 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
case "ping_desc":
|
||||
return "ORDER BY ping_ms IS NULL, ping_ms DESC";
|
||||
case "host_asc":
|
||||
return "ORDER BY LOWER(host) ASC";
|
||||
return "ORDER BY LOWER(COALESCE(nickname, host)) ASC";
|
||||
case "host_desc":
|
||||
return "ORDER BY LOWER(host) DESC";
|
||||
return "ORDER BY LOWER(COALESCE(nickname, host)) DESC";
|
||||
default:
|
||||
return "ORDER BY last_seen DESC";
|
||||
}
|
||||
@@ -302,7 +327,7 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
|
||||
const rows = db
|
||||
.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
|
||||
${whereSql}
|
||||
${orderBy}
|
||||
@@ -320,6 +345,7 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
arch: c.arch || "arch?",
|
||||
version: c.version || "0",
|
||||
user: c.user,
|
||||
nickname: c.nickname || null,
|
||||
monitors: c.monitors,
|
||||
country: c.country || "ZZ",
|
||||
pingMs: c.pingMs ?? null,
|
||||
|
||||
@@ -6,10 +6,12 @@ import {
|
||||
banIp,
|
||||
clientExists,
|
||||
deleteClientRow,
|
||||
getClientOnlineState,
|
||||
getClientIp,
|
||||
isIpBanned,
|
||||
listBannedIps,
|
||||
listClients,
|
||||
setClientNickname,
|
||||
setOnlineState,
|
||||
unbanIp,
|
||||
} from "../../db";
|
||||
@@ -238,6 +240,13 @@ export async function handleClientRoutes(
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
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);
|
||||
if (!target && !existsInDb) {
|
||||
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 });
|
||||
}
|
||||
|
||||
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") {
|
||||
const cmdMatch = url.pathname.match(/^\/api\/clients\/(.+)\/command$/);
|
||||
if (cmdMatch) {
|
||||
|
||||
@@ -15,6 +15,7 @@ export type ClientInfo = {
|
||||
arch?: string;
|
||||
version?: string;
|
||||
user?: string;
|
||||
nickname?: string;
|
||||
monitors?: number;
|
||||
monitorInfo?: { width: number; height: number }[];
|
||||
country?: string;
|
||||
|
||||
Reference in New Issue
Block a user