add chat system
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { authenticateRequest } from "../../auth";
|
||||
import { getConfig } from "../../config";
|
||||
import { getChatHistory, getOnlineClientCountForUser } from "../../db";
|
||||
import { hasPermission } from "../../users";
|
||||
|
||||
export async function handleChatRoutes(
|
||||
req: Request,
|
||||
url: URL,
|
||||
): Promise<Response | null> {
|
||||
if (req.method === "GET" && url.pathname === "/api/chat/history") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response(JSON.stringify({ error: "Not authenticated" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const beforeParam = url.searchParams.get("before");
|
||||
const limitParam = url.searchParams.get("limit");
|
||||
const before = beforeParam ? Number(beforeParam) : undefined;
|
||||
const limit = limitParam ? Number(limitParam) : 50;
|
||||
|
||||
if (before !== undefined && (isNaN(before) || before <= 0)) {
|
||||
return Response.json({ error: "Invalid 'before' parameter" }, { status: 400 });
|
||||
}
|
||||
if (isNaN(limit) || limit < 1 || limit > 200) {
|
||||
return Response.json({ error: "Invalid 'limit' parameter (1-200)" }, { status: 400 });
|
||||
}
|
||||
|
||||
const retDays = getConfig().chat?.retentionDays ?? 30;
|
||||
const retMs = retDays > 0 ? retDays * 24 * 60 * 60 * 1000 : undefined;
|
||||
const messages = getChatHistory(before, limit, retMs);
|
||||
const enriched = messages.map((m) => ({
|
||||
...m,
|
||||
onlineClients: m.userId ? getOnlineClientCountForUser(m.userId) : 0,
|
||||
}));
|
||||
const canWrite = hasPermission(user.role, "chat:write", user.userId);
|
||||
return Response.json({ messages: enriched, canWrite });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -850,6 +850,44 @@ export async function handleClientRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method === "PATCH" && url.pathname === "/api/clients/bulk-group") {
|
||||
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 });
|
||||
}
|
||||
|
||||
let body: any = {};
|
||||
try { body = await req.json(); } catch {
|
||||
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const clientIds = body?.clientIds;
|
||||
if (!Array.isArray(clientIds) || clientIds.length === 0 || clientIds.some((id: any) => typeof id !== "string")) {
|
||||
return Response.json({ error: "clientIds must be a non-empty array of strings" }, { status: 400 });
|
||||
}
|
||||
if (clientIds.length > 500) {
|
||||
return Response.json({ error: "Too many clients (max 500)" }, { 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 });
|
||||
}
|
||||
|
||||
let updated = 0;
|
||||
for (const cid of clientIds) {
|
||||
if (!canUserAccessClient(user.userId, user.role, cid)) continue;
|
||||
if (setClientGroup(cid, groupId)) updated++;
|
||||
}
|
||||
|
||||
notifyDashboardViewers();
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, details: `bulk_set_group:${groupId ?? "none"}:${updated}/${clientIds.length}`, success: true });
|
||||
return Response.json({ ok: true, updated, total: clientIds.length, groupId }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
const clientGroupMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/group$/);
|
||||
if (req.method === "PATCH" && clientGroupMatch) {
|
||||
const user = await authenticateRequest(req);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { authenticateRequest } from "../../auth";
|
||||
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
|
||||
import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfig, getExportableConfig, importFullConfig } from "../../config";
|
||||
import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfig, updateChatConfig, getExportableConfig, importFullConfig } from "../../config";
|
||||
import { getClientMetricsSummary, getClientMetricsSummaryForUser } from "../../db";
|
||||
import { metrics } from "../../metrics";
|
||||
import { requirePermission } from "../../rbac";
|
||||
@@ -493,6 +493,45 @@ export async function handleMiscRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -45,6 +45,8 @@ type WsLifecycleDeps = {
|
||||
handleKeyloggerViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleVoiceViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleNotificationViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleChatViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleChatViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleConsoleViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleRemoteDesktopViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleWebcamViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
@@ -151,6 +153,7 @@ export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLif
|
||||
if (role === "keylogger_viewer") return deps.handleKeyloggerViewerOpen(ws);
|
||||
if (role === "voice_viewer") return deps.handleVoiceViewerOpen(ws);
|
||||
if (role === "notifications_viewer") return deps.handleNotificationViewerOpen(ws);
|
||||
if (role === "chat_viewer") return deps.handleChatViewerOpen(ws);
|
||||
|
||||
const id = clientId || uuidv4();
|
||||
ws.data.clientId = id;
|
||||
@@ -202,6 +205,7 @@ export async function handleWebSocketMessage(
|
||||
if (socketRole === "voice_viewer") return deps.handleVoiceViewerMessage(ws, message);
|
||||
if (socketRole === "notifications_viewer") return;
|
||||
if (socketRole === "dashboard_viewer") return;
|
||||
if (socketRole === "chat_viewer") return deps.handleChatViewerMessage(ws, message);
|
||||
|
||||
const { clientId, ip } = ws.data;
|
||||
|
||||
@@ -740,6 +744,13 @@ export function handleWebSocketClose(
|
||||
return;
|
||||
}
|
||||
|
||||
if (role === "chat_viewer") {
|
||||
if (sessionId) {
|
||||
sessionManager.deleteChatSession(sessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentClient = clientManager.getClient(clientId);
|
||||
if (currentClient && currentClient.ws !== ws) {
|
||||
logger.info(`[close] ${clientId} code=${code} (superseded socket, skipping cleanup)`);
|
||||
|
||||
@@ -271,5 +271,17 @@ export async function handleWsUpgradeRoutes(
|
||||
return new Response("Upgrade failed", { status: 500 });
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/api/chat/ws") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
const ip = server.requestIP(req)?.address || "";
|
||||
if (server.upgrade(req, { data: { role: "chat_viewer", clientId: "", ip, userRole: user.role, userId: user.userId } })) {
|
||||
return new Response();
|
||||
}
|
||||
return new Response("Upgrade failed", { status: 500 });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user