From 2c6e9507b55efdedcf88f77a054475b8dd9daa51 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 23 Mar 2026 21:41:53 -0700 Subject: [PATCH] Connection log page working --- server/lib/ip.ts | 10 +- .../auditLogs/queryConnectionAuditLog.ts | 156 ++++++++++++++- .../newt/handleConnectionLogMessage.ts | 16 +- server/routers/auditLogs/types.ts | 16 ++ .../[orgId]/settings/logs/connection/page.tsx | 180 ++++++++++++++++-- 5 files changed, 349 insertions(+), 29 deletions(-) diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 3a29b8661..7f829bcef 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -581,6 +581,7 @@ export type SubnetProxyTargetV2 = { max: number; protocol: "tcp" | "udp"; }[]; + resourceId?: number; }; export function generateSubnetProxyTargetV2( @@ -617,7 +618,8 @@ export function generateSubnetProxyTargetV2( sourcePrefixes: [], destPrefix: destination, portRange, - disableIcmp + disableIcmp, + resourceId: siteResource.siteResourceId, }; } @@ -628,7 +630,8 @@ export function generateSubnetProxyTargetV2( destPrefix: `${siteResource.aliasAddress}/32`, rewriteTo: destination, portRange, - disableIcmp + disableIcmp, + resourceId: siteResource.siteResourceId, }; } } else if (siteResource.mode == "cidr") { @@ -636,7 +639,8 @@ export function generateSubnetProxyTargetV2( sourcePrefixes: [], destPrefix: siteResource.destination, portRange, - disableIcmp + disableIcmp, + resourceId: siteResource.siteResourceId, }; } diff --git a/server/private/routers/auditLogs/queryConnectionAuditLog.ts b/server/private/routers/auditLogs/queryConnectionAuditLog.ts index f321444cd..b638ed488 100644 --- a/server/private/routers/auditLogs/queryConnectionAuditLog.ts +++ b/server/private/routers/auditLogs/queryConnectionAuditLog.ts @@ -17,6 +17,7 @@ import { siteResources, sites, clients, + users, primaryDb } from "@server/db"; import { registry } from "@server/openApi"; @@ -193,6 +194,13 @@ async function enrichWithDetails( .filter((id): id is number => id !== null && id !== undefined) ) ]; + const userIds = [ + ...new Set( + logs + .map((log) => log.userId) + .filter((id): id is string => id !== null && id !== undefined) + ) + ]; // Fetch resource details from main database const resourceMap = new Map< @@ -235,18 +243,46 @@ async function enrichWithDetails( } // Fetch client details from main database - const clientMap = new Map(); + const clientMap = new Map< + number, + { name: string; niceId: string; type: string } + >(); if (clientIds.length > 0) { const clientDetails = await primaryDb .select({ clientId: clients.clientId, - name: clients.name + name: clients.name, + niceId: clients.niceId, + type: clients.type }) .from(clients) .where(inArray(clients.clientId, clientIds)); for (const c of clientDetails) { - clientMap.set(c.clientId, { name: c.name }); + clientMap.set(c.clientId, { + name: c.name, + niceId: c.niceId, + type: c.type + }); + } + } + + // Fetch user details from main database + const userMap = new Map< + string, + { email: string | null } + >(); + if (userIds.length > 0) { + const userDetails = await primaryDb + .select({ + userId: users.userId, + email: users.email + }) + .from(users) + .where(inArray(users.userId, userIds)); + + for (const u of userDetails) { + userMap.set(u.userId, { email: u.email }); } } @@ -267,6 +303,15 @@ async function enrichWithDetails( : null, clientName: log.clientId ? clientMap.get(log.clientId)?.name ?? null + : null, + clientNiceId: log.clientId + ? clientMap.get(log.clientId)?.niceId ?? null + : null, + clientType: log.clientId + ? clientMap.get(log.clientId)?.type ?? null + : null, + userEmail: log.userId + ? userMap.get(log.userId)?.email ?? null : null })); } @@ -290,10 +335,111 @@ async function queryUniqueFilterAttributes( .from(connectionAuditLog) .where(baseConditions); + // Get unique destination addresses + const uniqueDestAddrs = await logsDb + .selectDistinct({ + destAddr: connectionAuditLog.destAddr + }) + .from(connectionAuditLog) + .where(baseConditions); + + // Get unique client IDs + const uniqueClients = await logsDb + .selectDistinct({ + clientId: connectionAuditLog.clientId + }) + .from(connectionAuditLog) + .where(baseConditions); + + // Get unique resource IDs + const uniqueResources = await logsDb + .selectDistinct({ + siteResourceId: connectionAuditLog.siteResourceId + }) + .from(connectionAuditLog) + .where(baseConditions); + + // Get unique user IDs + const uniqueUsers = await logsDb + .selectDistinct({ + userId: connectionAuditLog.userId + }) + .from(connectionAuditLog) + .where(baseConditions); + + // Enrich client IDs with names from main database + const clientIds = uniqueClients + .map((row) => row.clientId) + .filter((id): id is number => id !== null); + + let clientsWithNames: Array<{ id: number; name: string }> = []; + if (clientIds.length > 0) { + const clientDetails = await primaryDb + .select({ + clientId: clients.clientId, + name: clients.name + }) + .from(clients) + .where(inArray(clients.clientId, clientIds)); + + clientsWithNames = clientDetails.map((c) => ({ + id: c.clientId, + name: c.name + })); + } + + // Enrich resource IDs with names from main database + const resourceIds = uniqueResources + .map((row) => row.siteResourceId) + .filter((id): id is number => id !== null); + + let resourcesWithNames: Array<{ id: number; name: string | null }> = []; + if (resourceIds.length > 0) { + const resourceDetails = await primaryDb + .select({ + siteResourceId: siteResources.siteResourceId, + name: siteResources.name + }) + .from(siteResources) + .where(inArray(siteResources.siteResourceId, resourceIds)); + + resourcesWithNames = resourceDetails.map((r) => ({ + id: r.siteResourceId, + name: r.name + })); + } + + // Enrich user IDs with emails from main database + const userIdsList = uniqueUsers + .map((row) => row.userId) + .filter((id): id is string => id !== null); + + let usersWithEmails: Array<{ id: string; email: string | null }> = []; + if (userIdsList.length > 0) { + const userDetails = await primaryDb + .select({ + userId: users.userId, + email: users.email + }) + .from(users) + .where(inArray(users.userId, userIdsList)); + + usersWithEmails = userDetails.map((u) => ({ + id: u.userId, + email: u.email + })); + } + return { protocols: uniqueProtocols .map((row) => row.protocol) - .filter((protocol): protocol is string => protocol !== null) + .filter((protocol): protocol is string => protocol !== null), + destAddrs: uniqueDestAddrs + .map((row) => row.destAddr) + .filter((addr): addr is string => addr !== null), + clients: clientsWithNames, + resources: resourcesWithNames, + users: usersWithEmails }; } @@ -342,7 +488,7 @@ export async function queryConnectionAuditLogs( const logsRaw = await baseQuery.limit(data.limit).offset(data.offset); - // Enrich with resource, site, and client details + // Enrich with resource, site, client, and user details const log = await enrichWithDetails(logsRaw); const totalCountResult = await countConnectionQuery(data); diff --git a/server/private/routers/newt/handleConnectionLogMessage.ts b/server/private/routers/newt/handleConnectionLogMessage.ts index 164c14488..2ac7153b5 100644 --- a/server/private/routers/newt/handleConnectionLogMessage.ts +++ b/server/private/routers/newt/handleConnectionLogMessage.ts @@ -277,6 +277,8 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { return; } + logger.debug(`Sessions: ${JSON.stringify(sessions)}`) + // Build a map from sourceAddr → { clientId, userId } by querying clients // whose subnet field matches exactly. Client subnets are stored with the // org's CIDR suffix (e.g. "100.90.128.5/16"), so we reconstruct that from @@ -295,9 +297,15 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { if (uniqueSourceAddrs.size > 0) { // Construct the exact subnet strings as stored in the DB const subnetQueries = Array.from(uniqueSourceAddrs).map( - (addr) => `${addr}${cidrSuffix}` + (addr) => { + // Strip port if present (e.g. "100.90.128.1:38004" → "100.90.128.1") + const ip = addr.includes(":") ? addr.split(":")[0] : addr; + return `${ip}${cidrSuffix}`; + } ); + logger.debug(`Subnet queries: ${JSON.stringify(subnetQueries)}`); + const matchedClients = await db .select({ clientId: clients.clientId, @@ -314,6 +322,7 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { for (const c of matchedClients) { const ip = c.subnet.split("/")[0]; + logger.debug(`Client ${c.clientId} subnet ${c.subnet} matches ${ip}`); ipToClient.set(ip, { clientId: c.clientId, userId: c.userId }); } } @@ -346,7 +355,10 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { // Match the source address to a client. The sourceAddr is the // client's IP on the WireGuard network, which corresponds to the IP // portion of the client's subnet CIDR (e.g. "100.90.128.5/24"). - const clientInfo = ipToClient.get(session.sourceAddr) ?? null; + // Strip port if present (e.g. "100.90.128.1:38004" → "100.90.128.1") + const sourceIp = session.sourceAddr.includes(":") ? session.sourceAddr.split(":")[0] : session.sourceAddr; + const clientInfo = ipToClient.get(sourceIp) ?? null; + buffer.push({ sessionId: session.sessionId, diff --git a/server/routers/auditLogs/types.ts b/server/routers/auditLogs/types.ts index 20e11e17b..4c278cba5 100644 --- a/server/routers/auditLogs/types.ts +++ b/server/routers/auditLogs/types.ts @@ -112,6 +112,9 @@ export type QueryConnectionAuditLogResponse = { siteName: string | null; siteNiceId: string | null; clientName: string | null; + clientNiceId: string | null; + clientType: string | null; + userEmail: string | null; }[]; pagination: { total: number; @@ -120,5 +123,18 @@ export type QueryConnectionAuditLogResponse = { }; filterAttributes: { protocols: string[]; + destAddrs: string[]; + clients: { + id: number; + name: string; + }[]; + resources: { + id: number; + name: string | null; + }[]; + users: { + id: string; + email: string | null; + }[]; }; }; diff --git a/src/app/[orgId]/settings/logs/connection/page.tsx b/src/app/[orgId]/settings/logs/connection/page.tsx index 737b1efd7..7ac137467 100644 --- a/src/app/[orgId]/settings/logs/connection/page.tsx +++ b/src/app/[orgId]/settings/logs/connection/page.tsx @@ -1,4 +1,5 @@ "use client"; +import { Button } from "@app/components/ui/button"; import { ColumnFilter } from "@app/components/ColumnFilter"; import { DateTimeValue } from "@app/components/DateTimePicker"; import { LogDataTable } from "@app/components/LogDataTable"; @@ -14,7 +15,8 @@ import { build } from "@server/build"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { ColumnDef } from "@tanstack/react-table"; import axios from "axios"; -import { Cable, Monitor, Server } from "lucide-react"; +import { ArrowUpRight, Laptop, User } from "lucide-react"; +import Link from "next/link"; import { useTranslations } from "next-intl"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState, useTransition } from "react"; @@ -57,15 +59,31 @@ export default function ConnectionLogsPage() { const [isExporting, startTransition] = useTransition(); const [filterAttributes, setFilterAttributes] = useState<{ protocols: string[]; + destAddrs: string[]; + clients: { id: number; name: string }[]; + resources: { id: number; name: string | null }[]; + users: { id: string; email: string | null }[]; }>({ - protocols: [] + protocols: [], + destAddrs: [], + clients: [], + resources: [], + users: [] }); // Filter states - unified object for all filters const [filters, setFilters] = useState<{ protocol?: string; + destAddr?: string; + clientId?: string; + siteResourceId?: string; + userId?: string; }>({ - protocol: searchParams.get("protocol") || undefined + protocol: searchParams.get("protocol") || undefined, + destAddr: searchParams.get("destAddr") || undefined, + clientId: searchParams.get("clientId") || undefined, + siteResourceId: searchParams.get("siteResourceId") || undefined, + userId: searchParams.get("userId") || undefined }); // Pagination state @@ -211,9 +229,7 @@ export default function ConnectionLogsPage() { endDate: DateTimeValue, page: number = currentPage, size: number = pageSize, - filtersParam?: { - protocol?: string; - } + filtersParam?: typeof filters ) => { console.log("Date range changed:", { startDate, endDate, page, size }); if (!isPaidUser(tierMatrix.connectionLogs)) { @@ -411,9 +427,41 @@ export default function ConnectionLogsPage() { { accessorKey: "resourceName", header: ({ column }) => { - return t("resource"); + return ( +
+ {t("resource")} + ({ + value: res.id.toString(), + label: res.name || "Unnamed Resource" + }))} + selectedValue={filters.siteResourceId} + onValueChange={(value) => + handleFilterChange("siteResourceId", value) + } + searchPlaceholder="Search..." + emptyMessage="None found" + /> +
+ ); }, cell: ({ row }) => { + if (row.original.resourceName && row.original.resourceNiceId) { + return ( + + + + ); + } return ( {row.original.resourceName ?? "—"} @@ -421,6 +469,86 @@ export default function ConnectionLogsPage() { ); } }, + { + accessorKey: "clientName", + header: ({ column }) => { + return ( +
+ {t("client")} + ({ + value: c.id.toString(), + label: c.name + }))} + selectedValue={filters.clientId} + onValueChange={(value) => + handleFilterChange("clientId", value) + } + searchPlaceholder="Search..." + emptyMessage="None found" + /> +
+ ); + }, + cell: ({ row }) => { + const clientType = row.original.clientType === "olm" ? "machine" : "user"; + if (row.original.clientName && row.original.clientNiceId) { + return ( + + + + ); + } + return ( + + {row.original.clientName ?? "—"} + + ); + } + }, + { + accessorKey: "userEmail", + header: ({ column }) => { + return ( +
+ {t("user")} + ({ + value: u.id, + label: u.email || u.id + }))} + selectedValue={filters.userId} + onValueChange={(value) => + handleFilterChange("userId", value) + } + searchPlaceholder="Search..." + emptyMessage="None found" + /> +
+ ); + }, + cell: ({ row }) => { + if (row.original.userEmail || row.original.userId) { + return ( + + + {row.original.userEmail ?? row.original.userId} + + ); + } + return ; + } + }, { accessorKey: "sourceAddr", header: ({ column }) => { @@ -437,7 +565,23 @@ export default function ConnectionLogsPage() { { accessorKey: "destAddr", header: ({ column }) => { - return t("destinationAddress"); + return ( +
+ {t("destinationAddress")} + ({ + value: addr, + label: addr + }))} + selectedValue={filters.destAddr} + onValueChange={(value) => + handleFilterChange("destAddr", value) + } + searchPlaceholder="Search..." + emptyMessage="None found" + /> +
+ ); }, cell: ({ row }) => { return ( @@ -470,10 +614,9 @@ export default function ConnectionLogsPage() {
-
- + {/*
Connection Details -
+
*/}
Session ID:{" "} @@ -518,10 +661,9 @@ export default function ConnectionLogsPage() {
-
- + {/*
Resource & Site -
+
*/}
Resource:{" "} {row.resourceName ?? "—"} @@ -548,10 +690,9 @@ export default function ConnectionLogsPage() {
-
- + {/*
Client & Transfer -
+
*/}
Client: {row.clientName ?? "—"} {row.clientId && ( @@ -561,7 +702,8 @@ export default function ConnectionLogsPage() { )}
- User ID: {row.userId ?? "—"} + User:{" "} + {row.userEmail ?? row.userId ?? "—"}
Bytes Sent (TX):{" "} @@ -627,4 +769,4 @@ export default function ConnectionLogsPage() { /> ); -} \ No newline at end of file +}