|
|
|
@@ -2,12 +2,13 @@ import { authenticateRequest } from "../../auth";
|
|
|
|
|
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
|
|
|
|
|
import { logger } from "../../logger";
|
|
|
|
|
import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfig, updateChatConfig, getExportableConfig, importFullConfig, updateRegistrationConfig, updateBuildRateLimitConfig, updateThumbnailsConfig } from "../../config";
|
|
|
|
|
import { getClientMetricsSummary, getClientMetricsSummaryForUser, getDatabaseFileSizeBytes } from "../../db";
|
|
|
|
|
import { getClientMetricsSummary, getClientMetricsSummaryForUser, getDatabaseFileSizeBytes, listClients } from "../../db";
|
|
|
|
|
import { getThumbnailStats } from "../../thumbnails";
|
|
|
|
|
import { getClientCount, getOnlineClients } from "../../clientManager";
|
|
|
|
|
import { metrics } from "../../metrics";
|
|
|
|
|
import { requirePermission } from "../../rbac";
|
|
|
|
|
import { getUserTelegramChatId, setUserTelegramChatId, getUserClientAccessScope, listUserClientRuleIdsByAccess, canUserAccessClient, getUserById } from "../../users";
|
|
|
|
|
import { buildClientGraph } from "../client-graph";
|
|
|
|
|
import { runCertbotSetup } from "../certbot-setup";
|
|
|
|
|
import {
|
|
|
|
|
getActiveProxies,
|
|
|
|
@@ -329,13 +330,59 @@ export async function handleMiscRoutes(
|
|
|
|
|
snapshot.sessions.fileBrowser = deps.getFileBrowserSessionCount();
|
|
|
|
|
snapshot.sessions.process = deps.getProcessSessionCount();
|
|
|
|
|
|
|
|
|
|
const history = metrics.getHistory();
|
|
|
|
|
const requestedHistoryLimit = Number(url.searchParams.get("historyLimit") || 240);
|
|
|
|
|
const historyLimit = Number.isFinite(requestedHistoryLimit)
|
|
|
|
|
? Math.max(1, Math.min(2000, requestedHistoryLimit))
|
|
|
|
|
: 240;
|
|
|
|
|
const history = metrics.getHistory().slice(-historyLimit);
|
|
|
|
|
|
|
|
|
|
return new Response(JSON.stringify({ snapshot, history }), {
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.method === "GET" && url.pathname === "/api/client-graph") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
|
|
|
|
|
|
|
|
|
const limit = Math.max(25, Math.min(500, Number(url.searchParams.get("limit") || 300)));
|
|
|
|
|
const search = (url.searchParams.get("q") || "").trim().toLowerCase();
|
|
|
|
|
const statusFilter = url.searchParams.get("status") || "all";
|
|
|
|
|
const groupFilter = url.searchParams.get("group") || "all";
|
|
|
|
|
let allowedClientIds: string[] | undefined;
|
|
|
|
|
let deniedClientIds: string[] | undefined;
|
|
|
|
|
|
|
|
|
|
if (user.role !== "admin") {
|
|
|
|
|
const scope = getUserClientAccessScope(user.userId);
|
|
|
|
|
if (scope === "none") {
|
|
|
|
|
return Response.json(buildClientGraph([]), { headers: deps.CORS_HEADERS });
|
|
|
|
|
}
|
|
|
|
|
if (scope === "allowlist") {
|
|
|
|
|
allowedClientIds = listUserClientRuleIdsByAccess(user.userId, "allow");
|
|
|
|
|
} else if (scope === "denylist") {
|
|
|
|
|
deniedClientIds = listUserClientRuleIdsByAccess(user.userId, "deny");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clients = listClients({
|
|
|
|
|
page: 1,
|
|
|
|
|
pageSize: limit,
|
|
|
|
|
search,
|
|
|
|
|
sort: "last_seen_desc",
|
|
|
|
|
statusFilter,
|
|
|
|
|
osFilter: "all",
|
|
|
|
|
countryFilter: "all",
|
|
|
|
|
enrollmentFilter: "approved",
|
|
|
|
|
groupFilter,
|
|
|
|
|
allowedClientIds,
|
|
|
|
|
deniedClientIds,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const graph = buildClientGraph(clients.items);
|
|
|
|
|
graph.summary.totalClients = clients.total;
|
|
|
|
|
return Response.json(graph, { headers: deps.CORS_HEADERS });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.pathname === "/health") {
|
|
|
|
|
return new Response("ok", { headers: deps.CORS_HEADERS });
|
|
|
|
|
}
|
|
|
|
@@ -385,7 +432,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
requirePermission(user, "system:configure");
|
|
|
|
|
requirePermission(user, "system:security");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
@@ -447,7 +494,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
requirePermission(user, "system:configure");
|
|
|
|
|
requirePermission(user, "system:tls");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
@@ -502,7 +549,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
requirePermission(user, "system:configure");
|
|
|
|
|
requirePermission(user, "system:tls");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
@@ -743,7 +790,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
return new Response("Unauthorized", { status: 401 });
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
requirePermission(user, "system:configure");
|
|
|
|
|
requirePermission(user, "system:export-import");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
@@ -776,7 +823,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
return new Response("Unauthorized", { status: 401 });
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
requirePermission(user, "system:configure");
|
|
|
|
|
requirePermission(user, "system:export-import");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
@@ -828,7 +875,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
requirePermission(user, "system:configure");
|
|
|
|
|
requirePermission(user, "system:chat");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
@@ -869,7 +916,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
return new Response("Unauthorized", { status: 401 });
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
requirePermission(user, "system:configure");
|
|
|
|
|
requirePermission(user, "system:appearance");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Response) return error;
|
|
|
|
|
return new Response("Forbidden", { status: 403 });
|
|
|
|
@@ -934,7 +981,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
if (url.pathname === "/api/settings/registration") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
|
|
|
|
try { requirePermission(user, "system:configure"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
|
|
|
|
try { requirePermission(user, "system:registration"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
|
|
|
|
|
|
|
|
|
if (req.method === "GET") {
|
|
|
|
|
return Response.json({ registration: getConfig().registration }, { headers: deps.CORS_HEADERS });
|
|
|
|
@@ -970,7 +1017,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
if (url.pathname === "/api/settings/build-rate-limit") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
|
|
|
|
try { requirePermission(user, "system:configure"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
|
|
|
|
try { requirePermission(user, "system:build-limits"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
|
|
|
|
|
|
|
|
|
if (req.method === "GET") {
|
|
|
|
|
return Response.json({ buildRateLimit: getConfig().buildRateLimit }, { headers: deps.CORS_HEADERS });
|
|
|
|
@@ -1011,7 +1058,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.method === "PUT") {
|
|
|
|
|
try { requirePermission(user, "system:configure"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
|
|
|
|
try { requirePermission(user, "system:thumbnails"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
|
|
|
|
|
|
|
|
|
let body: any = {};
|
|
|
|
|
try { body = await req.json(); } catch {
|
|
|
|
@@ -1040,7 +1087,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
if (req.method === "GET" && url.pathname === "/api/settings/health") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
|
|
|
|
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });
|
|
|
|
|
try { requirePermission(user, "system:health"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
|
|
|
|
|
|
|
|
|
const mem = process.memoryUsage();
|
|
|
|
|
return Response.json({
|
|
|
|
@@ -1069,7 +1116,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
if (req.method === "POST" && url.pathname === "/api/settings/gc") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
|
|
|
|
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });
|
|
|
|
|
try { requirePermission(user, "system:health:manage"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
|
|
|
|
|
|
|
|
|
const before = process.memoryUsage().heapUsed;
|
|
|
|
|
if (typeof Bun !== "undefined" && typeof (Bun as any).gc === "function") {
|
|
|
|
@@ -1083,7 +1130,7 @@ export async function handleMiscRoutes(
|
|
|
|
|
if (req.method === "POST" && url.pathname === "/api/settings/profile") {
|
|
|
|
|
const user = await authenticateRequest(req);
|
|
|
|
|
if (!user) return new Response("Unauthorized", { status: 401 });
|
|
|
|
|
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });
|
|
|
|
|
try { requirePermission(user, "system:profiler"); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); }
|
|
|
|
|
|
|
|
|
|
if (activeServerProfile) {
|
|
|
|
|
return Response.json({ error: "A server profile capture is already running." }, { status: 409 });
|
|
|
|
|