add bookmarking clients, fix sorting issues, add sorting by country and only showing specific countries.
This commit is contained in:
@@ -27,7 +27,8 @@ db.run(`
|
||||
country TEXT,
|
||||
last_seen INTEGER,
|
||||
online INTEGER,
|
||||
ping_ms INTEGER
|
||||
ping_ms INTEGER,
|
||||
bookmarked INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
`);
|
||||
try {
|
||||
@@ -48,6 +49,9 @@ try {
|
||||
try {
|
||||
db.run(`ALTER TABLE clients ADD COLUMN custom_tag_note TEXT`);
|
||||
} catch {}
|
||||
try {
|
||||
db.run(`ALTER TABLE clients ADD COLUMN bookmarked INTEGER NOT NULL DEFAULT 0`);
|
||||
} catch {}
|
||||
db.run(
|
||||
`CREATE INDEX IF NOT EXISTS idx_clients_online_last_seen ON clients(online, last_seen DESC);`,
|
||||
);
|
||||
@@ -294,6 +298,7 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
sort,
|
||||
statusFilter,
|
||||
osFilter,
|
||||
countryFilter,
|
||||
allowedClientIds,
|
||||
deniedClientIds,
|
||||
} = filters;
|
||||
@@ -319,6 +324,11 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
params.push(osFilter);
|
||||
}
|
||||
|
||||
if (countryFilter && countryFilter !== "all") {
|
||||
where.push("UPPER(COALESCE(country,'ZZ'))=?");
|
||||
params.push(countryFilter.toUpperCase());
|
||||
}
|
||||
|
||||
if (Array.isArray(allowedClientIds)) {
|
||||
if (allowedClientIds.length === 0) {
|
||||
where.push("1=0");
|
||||
@@ -336,17 +346,22 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
||||
|
||||
const orderBy = (() => {
|
||||
const bookmark = "bookmarked DESC";
|
||||
switch (sort) {
|
||||
case "ping_asc":
|
||||
return "ORDER BY ping_ms IS NULL, ping_ms ASC";
|
||||
return `ORDER BY ${bookmark}, ping_ms IS NULL, ping_ms ASC, id ASC`;
|
||||
case "ping_desc":
|
||||
return "ORDER BY ping_ms IS NULL, ping_ms DESC";
|
||||
return `ORDER BY ${bookmark}, ping_ms IS NULL, ping_ms DESC, id ASC`;
|
||||
case "host_asc":
|
||||
return "ORDER BY LOWER(COALESCE(nickname, host)) ASC";
|
||||
return `ORDER BY ${bookmark}, LOWER(COALESCE(nickname, host)) ASC, id ASC`;
|
||||
case "host_desc":
|
||||
return "ORDER BY LOWER(COALESCE(nickname, host)) DESC";
|
||||
return `ORDER BY ${bookmark}, LOWER(COALESCE(nickname, host)) DESC, id ASC`;
|
||||
case "country_asc":
|
||||
return `ORDER BY ${bookmark}, LOWER(COALESCE(country, 'zz')) ASC, id ASC`;
|
||||
case "country_desc":
|
||||
return `ORDER BY ${bookmark}, LOWER(COALESCE(country, 'zz')) DESC, id ASC`;
|
||||
default:
|
||||
return "ORDER BY last_seen DESC";
|
||||
return `ORDER BY ${bookmark}, last_seen DESC, id ASC`;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -362,7 +377,7 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
|
||||
const rows = db
|
||||
.query<any>(
|
||||
`SELECT id, hwid, role, host, os, arch, version, user, nickname, custom_tag as customTag, custom_tag_note as customTagNote, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs
|
||||
`SELECT id, hwid, role, host, os, arch, version, user, nickname, custom_tag as customTag, custom_tag_note as customTagNote, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs, bookmarked
|
||||
FROM clients
|
||||
${whereSql}
|
||||
${orderBy}
|
||||
@@ -387,6 +402,7 @@ export function listClients(filters: ListFilters): ListResult {
|
||||
country: c.country || "ZZ",
|
||||
pingMs: c.pingMs ?? null,
|
||||
online: c.online === 1,
|
||||
bookmarked: c.bookmarked === 1,
|
||||
thumbnail: getThumbnail(c.id),
|
||||
}));
|
||||
|
||||
@@ -591,6 +607,32 @@ export function clientExists(id: string): boolean {
|
||||
return !!row?.id;
|
||||
}
|
||||
|
||||
export function listDistinctCountries(): { code: string; count: number }[] {
|
||||
const rows = db
|
||||
.query<{ code: string; count: number }>(
|
||||
`SELECT UPPER(COALESCE(NULLIF(country, ''), 'ZZ')) as code, COUNT(*) as count
|
||||
FROM clients
|
||||
GROUP BY UPPER(COALESCE(NULLIF(country, ''), 'ZZ'))
|
||||
ORDER BY count DESC`,
|
||||
)
|
||||
.all();
|
||||
return rows.map((r) => ({ code: r.code, count: Number(r.count) || 0 }));
|
||||
}
|
||||
|
||||
export function setClientBookmark(id: string, bookmarked: boolean): boolean {
|
||||
const result = db.run(
|
||||
`UPDATE clients SET bookmarked=? WHERE id=?`,
|
||||
bookmarked ? 1 : 0,
|
||||
id,
|
||||
);
|
||||
return ((result as any)?.changes || 0) > 0;
|
||||
}
|
||||
|
||||
export function getClientBookmark(id: string): boolean {
|
||||
const row = db.query<{ bookmarked: number }>(`SELECT bookmarked FROM clients WHERE id=?`).get(id);
|
||||
return row?.bookmarked === 1;
|
||||
}
|
||||
|
||||
export interface BuildRecord {
|
||||
id: string;
|
||||
status: string;
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
isIpBanned,
|
||||
listBannedIps,
|
||||
listClients,
|
||||
listDistinctCountries,
|
||||
setClientBookmark,
|
||||
setClientNickname,
|
||||
setClientTag,
|
||||
setOnlineState,
|
||||
@@ -71,8 +73,9 @@ export async function handleClientRoutes(
|
||||
const sort = url.searchParams.get("sort") || "last_seen_desc";
|
||||
const statusFilter = url.searchParams.get("status") || "all";
|
||||
const osFilter = url.searchParams.get("os") || "all";
|
||||
const countryFilter = url.searchParams.get("country") || "all";
|
||||
if (user.role === "admin") {
|
||||
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter });
|
||||
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter, countryFilter });
|
||||
return Response.json(result, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
@@ -100,12 +103,20 @@ export async function handleClientRoutes(
|
||||
sort,
|
||||
statusFilter,
|
||||
osFilter,
|
||||
countryFilter,
|
||||
allowedClientIds,
|
||||
deniedClientIds,
|
||||
});
|
||||
return Response.json(result, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/clients/countries") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) return new Response("Unauthorized", { status: 401 });
|
||||
const countries = listDistinctCountries();
|
||||
return Response.json({ countries }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/clients/banned-ips") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) return new Response("Unauthorized", { status: 401 });
|
||||
@@ -398,6 +409,35 @@ export async function handleClientRoutes(
|
||||
);
|
||||
}
|
||||
|
||||
const bookmarkMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/bookmark$/);
|
||||
if (req.method === "PATCH" && bookmarkMatch) {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) return new Response("Unauthorized", { status: 401 });
|
||||
|
||||
const targetId = bookmarkMatch[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 bookmarked = !!body?.bookmarked;
|
||||
const updated = setClientBookmark(targetId, bookmarked);
|
||||
if (!updated) {
|
||||
return Response.json({ error: "Client not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json({ ok: true, bookmarked }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (req.method === "POST") {
|
||||
const cmdMatch = url.pathname.match(/^\/api\/clients\/(.+)\/command$/);
|
||||
if (cmdMatch) {
|
||||
|
||||
@@ -31,6 +31,7 @@ export type ListFilters = {
|
||||
sort: string;
|
||||
statusFilter?: string;
|
||||
osFilter?: string;
|
||||
countryFilter?: string;
|
||||
allowedClientIds?: string[];
|
||||
deniedClientIds?: string[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user