add downloading from urls and custom tags to clients.
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
listBannedIps,
|
||||
listClients,
|
||||
setClientNickname,
|
||||
setClientTag,
|
||||
setOnlineState,
|
||||
unbanIp,
|
||||
} from "../../db";
|
||||
@@ -334,6 +335,69 @@ export async function handleClientRoutes(
|
||||
return Response.json({ ok: true, nickname }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
const clientTagMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/tag$/);
|
||||
if (req.method === "PATCH" && clientTagMatch) {
|
||||
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 = clientTagMatch[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 rawTag = typeof body?.tag === "string" ? body.tag : "";
|
||||
const rawNote = typeof body?.note === "string" ? body.note : "";
|
||||
const tag = rawTag.trim();
|
||||
if (tag.length > 64) {
|
||||
return Response.json(
|
||||
{ error: "Tag must be 64 characters or fewer" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedTag = tag.length ? tag : null;
|
||||
const note = normalizedTag ? rawNote : null;
|
||||
const updated = setClientTag(targetId, normalizedTag, note);
|
||||
if (!updated) {
|
||||
return Response.json({ error: "Client not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({
|
||||
timestamp: Date.now(),
|
||||
username: user.username,
|
||||
ip,
|
||||
action: AuditAction.COMMAND,
|
||||
targetClientId: targetId,
|
||||
details: normalizedTag
|
||||
? `set_custom_tag:${normalizedTag} (note_len=${note?.length || 0})`
|
||||
: "clear_custom_tag",
|
||||
success: true,
|
||||
});
|
||||
|
||||
return Response.json(
|
||||
{ ok: true, tag: normalizedTag, note: note ?? null },
|
||||
{ headers: deps.CORS_HEADERS },
|
||||
);
|
||||
}
|
||||
|
||||
if (req.method === "POST") {
|
||||
const cmdMatch = url.pathname.match(/^\/api\/clients\/(.+)\/command$/);
|
||||
if (cmdMatch) {
|
||||
|
||||
@@ -81,6 +81,71 @@ export async function handleDeployRoutes(
|
||||
return Response.json({ ok: true, uploadId: id, os, name: filename, size: bytes.length });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/deploy/fetch-url") {
|
||||
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 new Response("Bad request", { status: 400 });
|
||||
}
|
||||
|
||||
const fileUrl = typeof body?.url === "string" ? body.url.trim() : "";
|
||||
if (!fileUrl) {
|
||||
return new Response("Missing url", { status: 400 });
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(fileUrl);
|
||||
} catch {
|
||||
return new Response("Invalid URL", { status: 400 });
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return new Response("Only http and https URLs are allowed", { status: 400 });
|
||||
}
|
||||
|
||||
const rawFilename = path.basename(parsed.pathname) || "download.bin";
|
||||
const filename = rawFilename.replace(/[^a-zA-Z0-9._\-]/g, "_").substring(0, 128) || "download.bin";
|
||||
|
||||
let fileBytes: Uint8Array;
|
||||
try {
|
||||
const fetchRes = await fetch(fileUrl);
|
||||
if (!fetchRes.ok) {
|
||||
return new Response(`Remote fetch failed: ${fetchRes.status}`, { status: 502 });
|
||||
}
|
||||
fileBytes = new Uint8Array(await fetchRes.arrayBuffer());
|
||||
} catch (err: any) {
|
||||
return new Response(`Failed to fetch URL: ${err?.message || "network error"}`, { status: 502 });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
await fs.mkdir(deps.DEPLOY_ROOT, { recursive: true });
|
||||
const folder = path.join(deps.DEPLOY_ROOT, id);
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
const targetPath = path.join(folder, filename);
|
||||
await fs.writeFile(targetPath, fileBytes);
|
||||
|
||||
const os = deps.detectUploadOs(filename, fileBytes);
|
||||
const entry: DeployUpload = {
|
||||
id,
|
||||
path: targetPath,
|
||||
name: filename,
|
||||
size: fileBytes.length,
|
||||
os,
|
||||
};
|
||||
deps.deployUploads.set(id, entry);
|
||||
|
||||
return Response.json({ ok: true, uploadId: id, os, name: filename, size: fileBytes.length });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/deploy/run") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
|
||||
Reference in New Issue
Block a user