Files
overlord/Overlord-Server/src/server/routes/misc-routes.ts
T

733 lines
25 KiB
TypeScript

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 } from "../../db";
import { metrics } from "../../metrics";
import { requirePermission } from "../../rbac";
import { getUserTelegramChatId, setUserTelegramChatId, getUserClientAccessScope, listUserClientRuleIdsByAccess, canUserAccessClient } from "../../users";
import { runCertbotSetup } from "../certbot-setup";
import {
getActiveProxies,
startProxy,
stopProxy,
} from "../socks5-proxy-manager";
type MiscRouteDeps = {
CORS_HEADERS: Record<string, string>;
SERVER_VERSION: string;
requestIP?: (req: Request) => { address?: string } | null | undefined;
getConsoleSessionCount: () => number;
getRdSessionCount: () => number;
getFileBrowserSessionCount: () => number;
getProcessSessionCount: () => number;
tlsCertPath?: string;
tlsSource?: "certbot" | "configured" | "self-signed";
};
export async function handleMiscRoutes(
req: Request,
url: URL,
deps: MiscRouteDeps,
): Promise<Response | null> {
if (req.method === "GET" && url.pathname === "/api/metrics") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const snapshot = metrics.getSnapshot();
const clientSummary = user.role === "admin"
? getClientMetricsSummary()
: getClientMetricsSummaryForUser(user.userId);
snapshot.clients.total = clientSummary.total;
snapshot.clients.online = clientSummary.online;
snapshot.clients.offline = clientSummary.total - clientSummary.online;
snapshot.clients.byOS = clientSummary.byOS;
snapshot.clients.byCountry = clientSummary.byCountry;
snapshot.sessions.console = deps.getConsoleSessionCount();
snapshot.sessions.remoteDesktop = deps.getRdSessionCount();
snapshot.sessions.fileBrowser = deps.getFileBrowserSessionCount();
snapshot.sessions.process = deps.getProcessSessionCount();
const history = metrics.getHistory();
return new Response(JSON.stringify({ snapshot, history }), {
headers: { "Content-Type": "application/json" },
});
}
if (url.pathname === "/health") {
return new Response("ok", { headers: deps.CORS_HEADERS });
}
if (req.method === "GET" && url.pathname === "/api/version") {
return new Response(JSON.stringify({ version: deps.SERVER_VERSION }), {
headers: {
...deps.CORS_HEADERS,
"Content-Type": "application/json",
},
});
}
if (url.pathname === "/api/settings/telegram") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (req.method === "GET") {
const chatId = getUserTelegramChatId(user.userId);
return Response.json({ telegramChatId: chatId || "" });
}
if (req.method === "PUT") {
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const chatId = typeof body?.telegramChatId === "string" ? body.telegramChatId.trim() : null;
const result = setUserTelegramChatId(user.userId, chatId || null);
if (!result.success) {
return Response.json({ error: result.error }, { status: 400 });
}
return Response.json({ success: true, telegramChatId: chatId || "" });
}
}
if (url.pathname === "/api/settings/security") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
if (req.method === "GET") {
return Response.json({ security: getConfig().security }, { headers: deps.CORS_HEADERS });
}
if (req.method === "PUT") {
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const updated = await updateSecurityConfig({
sessionTtlHours: Number(body?.sessionTtlHours),
loginMaxAttempts: Number(body?.loginMaxAttempts),
loginWindowMinutes: Number(body?.loginWindowMinutes),
loginLockoutMinutes: Number(body?.loginLockoutMinutes),
passwordMinLength: Number(body?.passwordMinLength),
passwordRequireUppercase: Boolean(body?.passwordRequireUppercase),
passwordRequireLowercase: Boolean(body?.passwordRequireLowercase),
passwordRequireNumber: Boolean(body?.passwordRequireNumber),
passwordRequireSymbol: Boolean(body?.passwordRequireSymbol),
});
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: "Updated security settings",
success: true,
});
return Response.json({ ok: true, security: updated }, { headers: deps.CORS_HEADERS });
}
}
if (url.pathname === "/api/settings/tls") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
if (req.method === "GET") {
return Response.json({ tls: getConfig().tls }, { headers: deps.CORS_HEADERS });
}
if (req.method === "PUT") {
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const updated = await updateTlsConfig({
certPath: typeof body?.certPath === "string" ? body.certPath : undefined,
keyPath: typeof body?.keyPath === "string" ? body.keyPath : undefined,
caPath: typeof body?.caPath === "string" ? body.caPath : undefined,
certbot: {
enabled: Boolean(body?.certbot?.enabled),
livePath: typeof body?.certbot?.livePath === "string" ? body.certbot.livePath : undefined,
domain: typeof body?.certbot?.domain === "string" ? body.certbot.domain : undefined,
certFileName:
typeof body?.certbot?.certFileName === "string" ? body.certbot.certFileName : undefined,
keyFileName:
typeof body?.certbot?.keyFileName === "string" ? body.certbot.keyFileName : undefined,
caFileName:
typeof body?.certbot?.caFileName === "string" ? body.certbot.caFileName : undefined,
},
});
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: "Updated TLS settings",
success: true,
});
return Response.json({ ok: true, tls: updated }, { headers: deps.CORS_HEADERS });
}
}
if (req.method === "POST" && url.pathname === "/api/settings/tls/certbot/setup") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const domain = String(body?.domain || "").trim();
const email = String(body?.email || "").trim();
const livePath = String(body?.livePath || "/etc/letsencrypt/live").trim() || "/etc/letsencrypt/live";
try {
const result = await runCertbotSetup({ domain, email, livePath });
const updated = await updateTlsConfig({
certbot: {
enabled: true,
livePath,
domain,
certFileName: "fullchain.pem",
keyFileName: "privkey.pem",
caFileName: "chain.pem",
},
});
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Ran certbot setup for domain ${domain}`,
success: true,
});
return Response.json(
{
ok: true,
tls: updated,
certbot: {
certPath: result.certPath,
keyPath: result.keyPath,
caPath: result.caPath,
output: result.output,
},
message:
"Certificate issued and certbot TLS mode enabled. Restart the server/container to load the new certificate.",
},
{ headers: deps.CORS_HEADERS },
);
} catch (error: any) {
logger.error("[TLS] certbot auto-setup failed", error);
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Certbot setup failed for domain ${domain}`,
success: false,
errorMessage: String(error?.message || error),
});
return Response.json(
{
ok: false,
error: String(error?.message || "Certbot setup failed"),
},
{ status: 400, headers: deps.CORS_HEADERS },
);
}
}
if (req.method === "GET" && url.pathname === "/api/audit-logs") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
try {
requirePermission(user, "audit:view");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const page = Math.max(1, Number(url.searchParams.get("page") || 1));
const pageSize = Math.max(1, Math.min(100, Number(url.searchParams.get("pageSize") || 50)));
const targetClientId = (url.searchParams.get("clientId") || "").trim();
const action = (url.searchParams.get("action") || "").trim();
const actionsRaw = (url.searchParams.get("actions") || "").trim();
const actions = actionsRaw
? actionsRaw
.split(",")
.map((entry) => entry.trim())
.filter(Boolean)
: undefined;
const startDate = Number(url.searchParams.get("startDate") || 0) || undefined;
const endDate = Number(url.searchParams.get("endDate") || 0) || undefined;
const successOnly = url.searchParams.get("successOnly") === "true";
let allowedClientIds: string[] | undefined;
let deniedClientIds: string[] | undefined;
if (user.role !== "admin") {
const scope = getUserClientAccessScope(user.userId);
if (scope === "none") {
return Response.json({ logs: [], total: 0, page, pageSize }, { headers: deps.CORS_HEADERS });
}
if (scope === "allowlist") {
allowedClientIds = listUserClientRuleIdsByAccess(user.userId, "allow");
} else if (scope === "denylist") {
deniedClientIds = listUserClientRuleIdsByAccess(user.userId, "deny");
}
}
const result = getAuditLogs({
page,
pageSize,
targetClientId: targetClientId || undefined,
action: action || undefined,
actions,
startDate,
endDate,
successOnly,
allowedClientIds,
deniedClientIds,
});
return Response.json(result, { headers: deps.CORS_HEADERS });
}
if (req.method === "GET" && url.pathname === "/api/proxy/list") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role === "viewer") {
return new Response("Forbidden", { status: 403 });
}
const proxies = getActiveProxies().filter((p) =>
canUserAccessClient(user.userId, user.role, p.clientId),
);
return Response.json({ proxies }, { headers: deps.CORS_HEADERS });
}
if (req.method === "POST" && url.pathname === "/api/proxy/start") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role === "viewer") {
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ ok: false, message: "Invalid JSON" }, { status: 400 });
}
const clientId = typeof body?.clientId === "string" ? body.clientId.trim() : "";
const port = typeof body?.port === "number" ? Math.floor(body.port) : 0;
if (!clientId) {
return Response.json({ ok: false, message: "clientId is required" }, { status: 400 });
}
if (!canUserAccessClient(user.userId, user.role, clientId)) {
return Response.json({ ok: false, message: "Forbidden: Client access denied" }, { status: 403 });
}
if (port < 1 || port > 65535) {
return Response.json({ ok: false, message: "port must be 1-65535" }, { status: 400 });
}
const result = startProxy(clientId, port);
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Started SOCKS5 proxy on port ${port} for client ${clientId}`,
success: result.ok,
errorMessage: result.ok ? undefined : result.message,
});
return Response.json(result, {
status: result.ok ? 200 : 400,
headers: deps.CORS_HEADERS,
});
}
if (req.method === "POST" && url.pathname === "/api/proxy/stop") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role === "viewer") {
return new Response("Forbidden", { status: 403 });
}
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ ok: false, message: "Invalid JSON" }, { status: 400 });
}
const port = typeof body?.port === "number" ? Math.floor(body.port) : 0;
if (port < 1 || port > 65535) {
return Response.json({ ok: false, message: "port must be 1-65535" }, { status: 400 });
}
const owner = getActiveProxies().find((p) => p.port === port);
if (owner && !canUserAccessClient(user.userId, user.role, owner.clientId)) {
return Response.json({ ok: false, message: "Forbidden: Client access denied" }, { status: 403 });
}
const result = stopProxy(port);
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Stopped SOCKS5 proxy on port ${port}${owner ? ` (client ${owner.clientId})` : ""}`,
success: result.ok,
errorMessage: result.ok ? undefined : result.message,
});
return Response.json(result, {
status: result.ok ? 200 : 400,
headers: deps.CORS_HEADERS,
});
}
if (req.method === "GET" && url.pathname === "/api/cert/info") {
return Response.json(
{ source: deps.tlsSource || "unknown" },
{ headers: { "Content-Type": "application/json", ...deps.CORS_HEADERS } },
);
}
if (req.method === "GET" && url.pathname === "/api/settings/export") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
const exportData = getExportableConfig(deps.SERVER_VERSION);
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: "Exported settings",
success: true,
});
const dateStr = new Date().toISOString().slice(0, 10);
return new Response(JSON.stringify(exportData, null, 2), {
headers: {
"Content-Type": "application/json",
"Content-Disposition": `attachment; filename="overlord-settings-${dateStr}.json"`,
...deps.CORS_HEADERS,
},
});
}
if (req.method === "POST" && url.pathname === "/api/settings/import") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
if (!body || typeof body !== "object") {
return Response.json({ error: "Expected a JSON object" }, { status: 400 });
}
try {
const result = await importFullConfig(body);
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Imported settings: ${result.applied.join(", ") || "none"}`,
success: true,
});
return Response.json({ ok: true, applied: result.applied, warnings: result.warnings }, { headers: deps.CORS_HEADERS });
} catch (error: any) {
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: "Settings import failed",
success: false,
errorMessage: String(error?.message || error),
});
return Response.json({ ok: false, error: String(error?.message || "Import failed") }, { status: 400, headers: deps.CORS_HEADERS });
}
}
if (url.pathname === "/api/settings/chat") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
if (req.method === "GET") {
return Response.json({ chat: getConfig().chat }, { headers: deps.CORS_HEADERS });
}
if (req.method === "PUT") {
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const updated = await updateChatConfig({
retentionDays: Number(body?.retentionDays),
});
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Updated chat settings: retention=${updated.retentionDays} days`,
success: true,
});
return Response.json({ ok: true, chat: updated }, { headers: deps.CORS_HEADERS });
}
}
if (url.pathname === "/api/settings/appearance") {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
if (req.method === "GET") {
return Response.json(
{ customCSS: getConfig().appearance?.customCSS || "" },
{ headers: deps.CORS_HEADERS },
);
}
if (req.method === "PUT") {
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const customCSS = typeof body?.customCSS === "string" ? body.customCSS : "";
if (customCSS.length > 51200) {
return Response.json({ error: "CSS exceeds 50 KB limit" }, { status: 400 });
}
const updated = await updateAppearanceConfig(customCSS);
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: "Updated custom CSS",
success: true,
});
return Response.json({ ok: true, customCSS: updated.customCSS }, { headers: deps.CORS_HEADERS });
}
}
if (req.method === "GET" && url.pathname === "/api/cert/download" && deps.tlsCertPath) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
try {
const file = Bun.file(deps.tlsCertPath);
if (await file.exists()) {
return new Response(file, {
headers: {
"Content-Type": "application/x-pem-file",
"Content-Disposition": 'attachment; filename="overlord-ca.crt"',
...deps.CORS_HEADERS,
},
});
}
} catch { }
return new Response("Certificate not available", { status: 404 });
}
// ── Registration settings (admin only) ──────────────────────────────
if (url.pathname === "/api/settings/registration") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden: Admin access required", { status: 403 });
if (req.method === "GET") {
return Response.json({ registration: getConfig().registration }, { headers: deps.CORS_HEADERS });
}
if (req.method === "PUT") {
let body: any = {};
try { body = await req.json(); } catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const updated = await updateRegistrationConfig({
mode: body?.mode,
defaultRole: body?.defaultRole,
maxUsersTotal: body?.maxUsersTotal !== undefined ? Number(body.maxUsersTotal) : undefined,
});
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Updated registration settings (mode: ${updated.mode})`,
success: true,
});
return Response.json({ ok: true, registration: updated }, { headers: deps.CORS_HEADERS });
}
}
// ── Build rate limit settings (admin only) ──────────────────────────
if (url.pathname === "/api/settings/build-rate-limit") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (user.role !== "admin") return new Response("Forbidden: Admin access required", { status: 403 });
if (req.method === "GET") {
return Response.json({ buildRateLimit: getConfig().buildRateLimit }, { headers: deps.CORS_HEADERS });
}
if (req.method === "PUT") {
let body: any = {};
try { body = await req.json(); } catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const updated = await updateBuildRateLimitConfig({
maxBuildsPerHour: body?.maxBuildsPerHour !== undefined ? Number(body.maxBuildsPerHour) : undefined,
maxConcurrentPerUser: body?.maxConcurrentPerUser !== undefined ? Number(body.maxConcurrentPerUser) : undefined,
globalMaxConcurrent: body?.globalMaxConcurrent !== undefined ? Number(body.globalMaxConcurrent) : undefined,
});
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: "Updated build rate limit settings",
success: true,
});
return Response.json({ ok: true, buildRateLimit: updated }, { headers: deps.CORS_HEADERS });
}
}
// ── Thumbnail toggles (GET: any user, PUT: admin only) ─────────────
if (url.pathname === "/api/settings/thumbnails") {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
if (req.method === "GET") {
return Response.json({ thumbnails: getConfig().thumbnails }, { headers: deps.CORS_HEADERS });
}
if (req.method === "PUT") {
if (user.role !== "admin") return new Response("Forbidden: Admin access required", { status: 403 });
let body: any = {};
try { body = await req.json(); } catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const updated = await updateThumbnailsConfig({
dashboardEnabled: body?.dashboardEnabled !== undefined ? Boolean(body.dashboardEnabled) : undefined,
wallEnabled: body?.wallEnabled !== undefined ? Boolean(body.wallEnabled) : undefined,
});
logAudit({
timestamp: Date.now(),
username: user.username,
ip: deps.requestIP?.(req)?.address || "unknown",
action: AuditAction.COMMAND,
details: `Updated thumbnail settings (dashboard=${updated.dashboardEnabled}, wall=${updated.wallEnabled})`,
success: true,
});
return Response.json({ ok: true, thumbnails: updated }, { headers: deps.CORS_HEADERS });
}
}
return null;
}