add grouping and make it obvious
This commit is contained in:
@@ -18,6 +18,12 @@ import {
|
||||
setClientTag,
|
||||
setOnlineState,
|
||||
unbanIp,
|
||||
listGroups,
|
||||
getGroup,
|
||||
createGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
setClientGroup,
|
||||
} from "../../db";
|
||||
import { metrics } from "../../metrics";
|
||||
import { encodeMessage } from "../../protocol";
|
||||
@@ -59,6 +65,7 @@ export async function handleClientRoutes(
|
||||
): Promise<Response | null> {
|
||||
if (
|
||||
!url.pathname.startsWith("/api/clients") &&
|
||||
!url.pathname.startsWith("/api/groups") &&
|
||||
!url.pathname.match(/^\/api\/clients\/.+\/command$/)
|
||||
) {
|
||||
return null;
|
||||
@@ -77,9 +84,10 @@ export async function handleClientRoutes(
|
||||
const osFilter = url.searchParams.get("os") || "all";
|
||||
const countryFilter = url.searchParams.get("country") || "all";
|
||||
const enrollmentFilter = url.searchParams.get("enrollmentFilter") || "approved";
|
||||
const groupFilter = url.searchParams.get("group") || "all";
|
||||
const isEnrollmentRequest = url.searchParams.has("enrollmentFilter");
|
||||
if (user.role === "admin") {
|
||||
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter, countryFilter, enrollmentFilter });
|
||||
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter, countryFilter, enrollmentFilter, groupFilter });
|
||||
return Response.json(result, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
@@ -93,6 +101,7 @@ export async function handleClientRoutes(
|
||||
osFilter,
|
||||
countryFilter,
|
||||
enrollmentFilter,
|
||||
groupFilter,
|
||||
builtByUserId: user.userId,
|
||||
requireBuildOwner: true,
|
||||
});
|
||||
@@ -125,6 +134,7 @@ export async function handleClientRoutes(
|
||||
osFilter,
|
||||
countryFilter,
|
||||
enrollmentFilter,
|
||||
groupFilter,
|
||||
allowedClientIds,
|
||||
deniedClientIds,
|
||||
});
|
||||
@@ -758,5 +768,124 @@ export async function handleClientRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/groups") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) return new Response("Unauthorized", { status: 401 });
|
||||
|
||||
if (req.method === "GET") {
|
||||
return Response.json({ groups: listGroups() }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (req.method === "POST") {
|
||||
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 name = typeof body?.name === "string" ? body.name.trim() : "";
|
||||
const color = typeof body?.color === "string" ? body.color.trim() : "#3b82f6";
|
||||
if (!name || name.length > 64) {
|
||||
return Response.json({ error: "Name is required and must be 64 characters or fewer" }, { status: 400 });
|
||||
}
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(color)) {
|
||||
return Response.json({ error: "Color must be a valid hex code (#RRGGBB)" }, { status: 400 });
|
||||
}
|
||||
|
||||
const group = createGroup(name, color);
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, details: `create_group:${name}`, success: true });
|
||||
return Response.json(group, { status: 201, headers: deps.CORS_HEADERS });
|
||||
}
|
||||
}
|
||||
|
||||
const groupIdMatch = url.pathname.match(/^\/api\/groups\/(\d+)$/);
|
||||
if (groupIdMatch) {
|
||||
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 groupId = Number(groupIdMatch[1]);
|
||||
|
||||
if (req.method === "PATCH") {
|
||||
let body: any = {};
|
||||
try { body = await req.json(); } catch {
|
||||
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const existing = getGroup(groupId);
|
||||
if (!existing) return Response.json({ error: "Group not found" }, { status: 404 });
|
||||
|
||||
const name = typeof body?.name === "string" ? body.name.trim() : existing.name;
|
||||
const color = typeof body?.color === "string" ? body.color.trim() : existing.color;
|
||||
if (name.length === 0 || name.length > 64) {
|
||||
return Response.json({ error: "Name must be 1-64 characters" }, { status: 400 });
|
||||
}
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(color)) {
|
||||
return Response.json({ error: "Color must be a valid hex code (#RRGGBB)" }, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = updateGroup(groupId, name, color);
|
||||
if (!updated) return Response.json({ error: "Group not found" }, { status: 404 });
|
||||
notifyDashboardViewers();
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, details: `update_group:${groupId}`, success: true });
|
||||
return Response.json({ ok: true, id: groupId, name, color }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (req.method === "DELETE") {
|
||||
const deleted = deleteGroup(groupId);
|
||||
if (!deleted) return Response.json({ error: "Group not found" }, { status: 404 });
|
||||
notifyDashboardViewers();
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, details: `delete_group:${groupId}`, success: true });
|
||||
return Response.json({ ok: true }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
}
|
||||
|
||||
const clientGroupMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/group$/);
|
||||
if (req.method === "PATCH" && clientGroupMatch) {
|
||||
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 = clientGroupMatch[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 groupId = body?.groupId === null ? null : Number(body?.groupId);
|
||||
if (groupId !== null && (isNaN(groupId) || groupId < 1)) {
|
||||
return Response.json({ error: "Invalid groupId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = setClientGroup(targetId, groupId);
|
||||
if (!updated) {
|
||||
return Response.json({ error: "Client not found or group does not exist" }, { status: 404 });
|
||||
}
|
||||
notifyDashboardViewers();
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, targetClientId: targetId, details: groupId ? `set_group:${groupId}` : "clear_group", success: true });
|
||||
return Response.json({ ok: true, groupId }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user