Connection log page working

This commit is contained in:
Owen
2026-03-23 21:41:53 -07:00
parent 6471571bc6
commit 2c6e9507b5
5 changed files with 349 additions and 29 deletions

View File

@@ -581,6 +581,7 @@ export type SubnetProxyTargetV2 = {
max: number; max: number;
protocol: "tcp" | "udp"; protocol: "tcp" | "udp";
}[]; }[];
resourceId?: number;
}; };
export function generateSubnetProxyTargetV2( export function generateSubnetProxyTargetV2(
@@ -617,7 +618,8 @@ export function generateSubnetProxyTargetV2(
sourcePrefixes: [], sourcePrefixes: [],
destPrefix: destination, destPrefix: destination,
portRange, portRange,
disableIcmp disableIcmp,
resourceId: siteResource.siteResourceId,
}; };
} }
@@ -628,7 +630,8 @@ export function generateSubnetProxyTargetV2(
destPrefix: `${siteResource.aliasAddress}/32`, destPrefix: `${siteResource.aliasAddress}/32`,
rewriteTo: destination, rewriteTo: destination,
portRange, portRange,
disableIcmp disableIcmp,
resourceId: siteResource.siteResourceId,
}; };
} }
} else if (siteResource.mode == "cidr") { } else if (siteResource.mode == "cidr") {
@@ -636,7 +639,8 @@ export function generateSubnetProxyTargetV2(
sourcePrefixes: [], sourcePrefixes: [],
destPrefix: siteResource.destination, destPrefix: siteResource.destination,
portRange, portRange,
disableIcmp disableIcmp,
resourceId: siteResource.siteResourceId,
}; };
} }

View File

@@ -17,6 +17,7 @@ import {
siteResources, siteResources,
sites, sites,
clients, clients,
users,
primaryDb primaryDb
} from "@server/db"; } from "@server/db";
import { registry } from "@server/openApi"; import { registry } from "@server/openApi";
@@ -193,6 +194,13 @@ async function enrichWithDetails(
.filter((id): id is number => id !== null && id !== undefined) .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 // Fetch resource details from main database
const resourceMap = new Map< const resourceMap = new Map<
@@ -235,18 +243,46 @@ async function enrichWithDetails(
} }
// Fetch client details from main database // Fetch client details from main database
const clientMap = new Map<number, { name: string }>(); const clientMap = new Map<
number,
{ name: string; niceId: string; type: string }
>();
if (clientIds.length > 0) { if (clientIds.length > 0) {
const clientDetails = await primaryDb const clientDetails = await primaryDb
.select({ .select({
clientId: clients.clientId, clientId: clients.clientId,
name: clients.name name: clients.name,
niceId: clients.niceId,
type: clients.type
}) })
.from(clients) .from(clients)
.where(inArray(clients.clientId, clientIds)); .where(inArray(clients.clientId, clientIds));
for (const c of clientDetails) { 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, : null,
clientName: log.clientId clientName: log.clientId
? clientMap.get(log.clientId)?.name ?? null ? 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 : null
})); }));
} }
@@ -290,10 +335,111 @@ async function queryUniqueFilterAttributes(
.from(connectionAuditLog) .from(connectionAuditLog)
.where(baseConditions); .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 { return {
protocols: uniqueProtocols protocols: uniqueProtocols
.map((row) => row.protocol) .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); 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 log = await enrichWithDetails(logsRaw);
const totalCountResult = await countConnectionQuery(data); const totalCountResult = await countConnectionQuery(data);

View File

@@ -277,6 +277,8 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
return; return;
} }
logger.debug(`Sessions: ${JSON.stringify(sessions)}`)
// Build a map from sourceAddr → { clientId, userId } by querying clients // Build a map from sourceAddr → { clientId, userId } by querying clients
// whose subnet field matches exactly. Client subnets are stored with the // 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 // 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) { if (uniqueSourceAddrs.size > 0) {
// Construct the exact subnet strings as stored in the DB // Construct the exact subnet strings as stored in the DB
const subnetQueries = Array.from(uniqueSourceAddrs).map( 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 const matchedClients = await db
.select({ .select({
clientId: clients.clientId, clientId: clients.clientId,
@@ -314,6 +322,7 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
for (const c of matchedClients) { for (const c of matchedClients) {
const ip = c.subnet.split("/")[0]; 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 }); 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 // Match the source address to a client. The sourceAddr is the
// client's IP on the WireGuard network, which corresponds to the IP // 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"). // 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({ buffer.push({
sessionId: session.sessionId, sessionId: session.sessionId,

View File

@@ -112,6 +112,9 @@ export type QueryConnectionAuditLogResponse = {
siteName: string | null; siteName: string | null;
siteNiceId: string | null; siteNiceId: string | null;
clientName: string | null; clientName: string | null;
clientNiceId: string | null;
clientType: string | null;
userEmail: string | null;
}[]; }[];
pagination: { pagination: {
total: number; total: number;
@@ -120,5 +123,18 @@ export type QueryConnectionAuditLogResponse = {
}; };
filterAttributes: { filterAttributes: {
protocols: string[]; protocols: string[];
destAddrs: string[];
clients: {
id: number;
name: string;
}[];
resources: {
id: number;
name: string | null;
}[];
users: {
id: string;
email: string | null;
}[];
}; };
}; };

View File

@@ -1,4 +1,5 @@
"use client"; "use client";
import { Button } from "@app/components/ui/button";
import { ColumnFilter } from "@app/components/ColumnFilter"; import { ColumnFilter } from "@app/components/ColumnFilter";
import { DateTimeValue } from "@app/components/DateTimePicker"; import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable"; import { LogDataTable } from "@app/components/LogDataTable";
@@ -14,7 +15,8 @@ import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import axios from "axios"; 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 { useTranslations } from "next-intl";
import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState, useTransition } from "react"; import { useEffect, useState, useTransition } from "react";
@@ -57,15 +59,31 @@ export default function ConnectionLogsPage() {
const [isExporting, startTransition] = useTransition(); const [isExporting, startTransition] = useTransition();
const [filterAttributes, setFilterAttributes] = useState<{ const [filterAttributes, setFilterAttributes] = useState<{
protocols: string[]; 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 // Filter states - unified object for all filters
const [filters, setFilters] = useState<{ const [filters, setFilters] = useState<{
protocol?: string; 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 // Pagination state
@@ -211,9 +229,7 @@ export default function ConnectionLogsPage() {
endDate: DateTimeValue, endDate: DateTimeValue,
page: number = currentPage, page: number = currentPage,
size: number = pageSize, size: number = pageSize,
filtersParam?: { filtersParam?: typeof filters
protocol?: string;
}
) => { ) => {
console.log("Date range changed:", { startDate, endDate, page, size }); console.log("Date range changed:", { startDate, endDate, page, size });
if (!isPaidUser(tierMatrix.connectionLogs)) { if (!isPaidUser(tierMatrix.connectionLogs)) {
@@ -411,9 +427,41 @@ export default function ConnectionLogsPage() {
{ {
accessorKey: "resourceName", accessorKey: "resourceName",
header: ({ column }) => { header: ({ column }) => {
return t("resource"); return (
<div className="flex items-center gap-2">
<span>{t("resource")}</span>
<ColumnFilter
options={filterAttributes.resources.map((res) => ({
value: res.id.toString(),
label: res.name || "Unnamed Resource"
}))}
selectedValue={filters.siteResourceId}
onValueChange={(value) =>
handleFilterChange("siteResourceId", value)
}
searchPlaceholder="Search..."
emptyMessage="None found"
/>
</div>
);
}, },
cell: ({ row }) => { cell: ({ row }) => {
if (row.original.resourceName && row.original.resourceNiceId) {
return (
<Link
href={`/${row.original.orgId}/settings/resources/proxy/${row.original.resourceNiceId}`}
>
<Button
variant="outline"
size="sm"
className="text-xs h-6"
>
{row.original.resourceName}
<ArrowUpRight className="ml-2 h-3 w-3" />
</Button>
</Link>
);
}
return ( return (
<span className="whitespace-nowrap"> <span className="whitespace-nowrap">
{row.original.resourceName ?? "—"} {row.original.resourceName ?? "—"}
@@ -421,6 +469,86 @@ export default function ConnectionLogsPage() {
); );
} }
}, },
{
accessorKey: "clientName",
header: ({ column }) => {
return (
<div className="flex items-center gap-2">
<span>{t("client")}</span>
<ColumnFilter
options={filterAttributes.clients.map((c) => ({
value: c.id.toString(),
label: c.name
}))}
selectedValue={filters.clientId}
onValueChange={(value) =>
handleFilterChange("clientId", value)
}
searchPlaceholder="Search..."
emptyMessage="None found"
/>
</div>
);
},
cell: ({ row }) => {
const clientType = row.original.clientType === "olm" ? "machine" : "user";
if (row.original.clientName && row.original.clientNiceId) {
return (
<Link
href={`/${row.original.orgId}/settings/clients/${clientType}/${row.original.clientNiceId}`}
>
<Button
variant="outline"
size="sm"
className="text-xs h-6"
>
<Laptop className="mr-1 h-3 w-3" />
{row.original.clientName}
<ArrowUpRight className="ml-2 h-3 w-3" />
</Button>
</Link>
);
}
return (
<span className="whitespace-nowrap">
{row.original.clientName ?? "—"}
</span>
);
}
},
{
accessorKey: "userEmail",
header: ({ column }) => {
return (
<div className="flex items-center gap-2">
<span>{t("user")}</span>
<ColumnFilter
options={filterAttributes.users.map((u) => ({
value: u.id,
label: u.email || u.id
}))}
selectedValue={filters.userId}
onValueChange={(value) =>
handleFilterChange("userId", value)
}
searchPlaceholder="Search..."
emptyMessage="None found"
/>
</div>
);
},
cell: ({ row }) => {
if (row.original.userEmail || row.original.userId) {
return (
<span className="flex items-center gap-1 whitespace-nowrap">
<User className="h-4 w-4" />
{row.original.userEmail ?? row.original.userId}
</span>
);
}
return <span></span>;
}
},
{ {
accessorKey: "sourceAddr", accessorKey: "sourceAddr",
header: ({ column }) => { header: ({ column }) => {
@@ -437,7 +565,23 @@ export default function ConnectionLogsPage() {
{ {
accessorKey: "destAddr", accessorKey: "destAddr",
header: ({ column }) => { header: ({ column }) => {
return t("destinationAddress"); return (
<div className="flex items-center gap-2">
<span>{t("destinationAddress")}</span>
<ColumnFilter
options={filterAttributes.destAddrs.map((addr) => ({
value: addr,
label: addr
}))}
selectedValue={filters.destAddr}
onValueChange={(value) =>
handleFilterChange("destAddr", value)
}
searchPlaceholder="Search..."
emptyMessage="None found"
/>
</div>
);
}, },
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
@@ -470,10 +614,9 @@ export default function ConnectionLogsPage() {
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-1 font-semibold text-sm mb-1"> {/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
<Cable className="h-4 w-4" />
Connection Details Connection Details
</div> </div>*/}
<div> <div>
<strong>Session ID:</strong>{" "} <strong>Session ID:</strong>{" "}
<span className="font-mono"> <span className="font-mono">
@@ -518,10 +661,9 @@ export default function ConnectionLogsPage() {
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-1 font-semibold text-sm mb-1"> {/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
<Server className="h-4 w-4" />
Resource & Site Resource & Site
</div> </div>*/}
<div> <div>
<strong>Resource:</strong>{" "} <strong>Resource:</strong>{" "}
{row.resourceName ?? "—"} {row.resourceName ?? "—"}
@@ -548,10 +690,9 @@ export default function ConnectionLogsPage() {
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-1 font-semibold text-sm mb-1"> {/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
<Monitor className="h-4 w-4" />
Client & Transfer Client & Transfer
</div> </div>*/}
<div> <div>
<strong>Client:</strong> {row.clientName ?? "—"} <strong>Client:</strong> {row.clientName ?? "—"}
{row.clientId && ( {row.clientId && (
@@ -561,7 +702,8 @@ export default function ConnectionLogsPage() {
)} )}
</div> </div>
<div> <div>
<strong>User ID:</strong> {row.userId ?? ""} <strong>User:</strong>{" "}
{row.userEmail ?? row.userId ?? "—"}
</div> </div>
<div> <div>
<strong>Bytes Sent (TX):</strong>{" "} <strong>Bytes Sent (TX):</strong>{" "}
@@ -627,4 +769,4 @@ export default function ConnectionLogsPage() {
/> />
</> </>
); );
} }