mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-25 20:16:40 +00:00
Connection log page working
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<number, { name: string }>();
|
||||
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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<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 }) => {
|
||||
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 (
|
||||
<span className="whitespace-nowrap">
|
||||
{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",
|
||||
header: ({ column }) => {
|
||||
@@ -437,7 +565,23 @@ export default function ConnectionLogsPage() {
|
||||
{
|
||||
accessorKey: "destAddr",
|
||||
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 }) => {
|
||||
return (
|
||||
@@ -470,10 +614,9 @@ export default function ConnectionLogsPage() {
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
||||
<Cable className="h-4 w-4" />
|
||||
{/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
||||
Connection Details
|
||||
</div>
|
||||
</div>*/}
|
||||
<div>
|
||||
<strong>Session ID:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
@@ -518,10 +661,9 @@ export default function ConnectionLogsPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
||||
<Server className="h-4 w-4" />
|
||||
{/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
||||
Resource & Site
|
||||
</div>
|
||||
</div>*/}
|
||||
<div>
|
||||
<strong>Resource:</strong>{" "}
|
||||
{row.resourceName ?? "—"}
|
||||
@@ -548,10 +690,9 @@ export default function ConnectionLogsPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
||||
<Monitor className="h-4 w-4" />
|
||||
{/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
||||
Client & Transfer
|
||||
</div>
|
||||
</div>*/}
|
||||
<div>
|
||||
<strong>Client:</strong> {row.clientName ?? "—"}
|
||||
{row.clientId && (
|
||||
@@ -561,7 +702,8 @@ export default function ConnectionLogsPage() {
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<strong>User ID:</strong> {row.userId ?? "—"}
|
||||
<strong>User:</strong>{" "}
|
||||
{row.userEmail ?? row.userId ?? "—"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Bytes Sent (TX):</strong>{" "}
|
||||
|
||||
Reference in New Issue
Block a user