From 921285e5b1807c758bc3d084292f521c41fa7c84 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 23 Oct 2025 15:33:29 -0700 Subject: [PATCH] Filtering on all tables --- messages/en-US.json | 3 +- .../routers/auditLogs/queryAccessAuditLog.ts | 2 + .../routers/auditLogs/queryActionAuditLog.ts | 41 +++- .../routers/auditLogs/queryRequstAuditLog.ts | 2 + server/routers/auditLogs/types.ts | 3 + src/actions/server.ts | 2 + src/app/[orgId]/settings/logs/access/page.tsx | 14 +- src/app/[orgId]/settings/logs/action/page.tsx | 176 ++++++++++++++++-- .../[orgId]/settings/logs/request/page.tsx | 30 ++- 9 files changed, 242 insertions(+), 31 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 801b5d5b..9157e84c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1920,5 +1920,6 @@ "reason": "Reason", "requestLogs": "Request Logs", "host": "Host", - "location": "Location" + "location": "Location", + "actionLogs": "Action Logs" } diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts index 92f927fd..d6c9dea6 100644 --- a/server/private/routers/auditLogs/queryAccessAuditLog.ts +++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts @@ -55,6 +55,7 @@ export const queryAccessAuditLogsQuery = z.object({ .optional(), actor: z.string().optional(), type: z.string().optional(), + location: z.string().optional(), limit: z .string() .optional() @@ -91,6 +92,7 @@ function getWhere(data: Q) { ? eq(accessAuditLog.actorType, data.actorType) : undefined, data.actorId ? eq(accessAuditLog.actorId, data.actorId) : undefined, + data.location ? eq(accessAuditLog.location, data.location) : undefined, data.type ? eq(accessAuditLog.type, data.type) : undefined, data.action !== undefined ? eq(accessAuditLog.action, data.action) diff --git a/server/private/routers/auditLogs/queryActionAuditLog.ts b/server/private/routers/auditLogs/queryActionAuditLog.ts index 6e785c76..f9dcbbf5 100644 --- a/server/private/routers/auditLogs/queryActionAuditLog.ts +++ b/server/private/routers/auditLogs/queryActionAuditLog.ts @@ -103,6 +103,38 @@ export function countActionQuery(data: Q) { return countQuery; } +async function queryUniqueFilterAttributes( + timeStart: number, + timeEnd: number, + orgId: string +) { + const baseConditions = and( + gt(actionAuditLog.timestamp, timeStart), + lt(actionAuditLog.timestamp, timeEnd), + eq(actionAuditLog.orgId, orgId) + ); + + // Get unique actors + const uniqueActors = await db + .selectDistinct({ + actor: actionAuditLog.actor + }) + .from(actionAuditLog) + .where(baseConditions); + + const uniqueActions = await db + .selectDistinct({ + action: actionAuditLog.action + }) + .from(actionAuditLog) + .where(baseConditions); + + return { + actors: uniqueActors.map(row => row.actor).filter((actor): actor is string => actor !== null), + actions: uniqueActions.map(row => row.action).filter((action): action is string => action !== null), + }; +} + registry.registerPath({ method: "get", path: "/org/{orgId}/logs/action", @@ -149,6 +181,12 @@ export async function queryActionAuditLogs( const totalCountResult = await countActionQuery(data); const totalCount = totalCountResult[0].count; + const filterAttributes = await queryUniqueFilterAttributes( + data.timeStart, + data.timeEnd, + data.orgId + ); + return response(res, { data: { log: log, @@ -156,7 +194,8 @@ export async function queryActionAuditLogs( total: totalCount, limit: data.limit, offset: data.offset - } + }, + filterAttributes }, success: true, error: false, diff --git a/server/routers/auditLogs/queryRequstAuditLog.ts b/server/routers/auditLogs/queryRequstAuditLog.ts index 283e3d7e..d41b14b6 100644 --- a/server/routers/auditLogs/queryRequstAuditLog.ts +++ b/server/routers/auditLogs/queryRequstAuditLog.ts @@ -46,6 +46,7 @@ export const queryAccessAuditLogsQuery = z.object({ .pipe(z.number().int().positive()) .optional(), actor: z.string().optional(), + location: z.string().optional(), host: z.string().optional(), path: z.string().optional(), limit: z @@ -82,6 +83,7 @@ function getWhere(data: Q) { data.method ? eq(requestAuditLog.method, data.method) : undefined, data.reason ? eq(requestAuditLog.reason, data.reason) : undefined, data.host ? eq(requestAuditLog.host, data.host) : undefined, + data.location ? eq(requestAuditLog.location, data.location) : undefined, data.path ? eq(requestAuditLog.path, data.path) : undefined, data.action !== undefined ? eq(requestAuditLog.action, data.action) diff --git a/server/routers/auditLogs/types.ts b/server/routers/auditLogs/types.ts index 8adc1750..81cef733 100644 --- a/server/routers/auditLogs/types.ts +++ b/server/routers/auditLogs/types.ts @@ -13,6 +13,9 @@ export type QueryActionAuditLogResponse = { limit: number; offset: number; }; + filterAttributes: { + actors: string[]; + }; }; export type QueryRequestAuditLogResponse = { diff --git a/src/actions/server.ts b/src/actions/server.ts index 5fbe1301..b75c3ed7 100644 --- a/src/actions/server.ts +++ b/src/actions/server.ts @@ -80,10 +80,12 @@ async function makeApiRequest( const headersList = await reqHeaders(); const host = headersList.get("host"); + const xForwardedFor = headersList.get("x-forwarded-for"); const headers: Record = { "Content-Type": "application/json", "X-CSRF-Token": "x-csrf-protection", + ...(xForwardedFor ? { "X-Forwarded-For": xForwardedFor } : {}), ...(cookieHeader && { Cookie: cookieHeader }), ...additionalHeaders }; diff --git a/src/app/[orgId]/settings/logs/access/page.tsx b/src/app/[orgId]/settings/logs/access/page.tsx index 47544ff6..d58dd09d 100644 --- a/src/app/[orgId]/settings/logs/access/page.tsx +++ b/src/app/[orgId]/settings/logs/access/page.tsx @@ -142,8 +142,6 @@ export default function GeneralPage() { filterType: keyof typeof filters, value: string | undefined ) => { - console.log(`${filterType} filter changed:`, value); - // Create new filters object with updated value const newFilters = { ...filters, @@ -193,6 +191,9 @@ export default function GeneralPage() { filtersParam?: { action?: string; type?: string; + resourceId?: string; + location?: string; + actor?: string; } ) => { console.log("Date range changed:", { startDate, endDate, page, size }); @@ -403,7 +404,7 @@ export default function GeneralPage() { {row.original.location ? ( - ({row.original.location}) + {row.original.location} ) : ( @@ -482,7 +483,12 @@ export default function GeneralPage() { }, cell: ({ row }) => { // should be capitalized first letter - return {row.original.type.charAt(0).toUpperCase() + row.original.type.slice(1) || "-"}; + return ( + + {row.original.type.charAt(0).toUpperCase() + + row.original.type.slice(1) || "-"} + + ); } }, { diff --git a/src/app/[orgId]/settings/logs/action/page.tsx b/src/app/[orgId]/settings/logs/action/page.tsx index a332f0cb..08dffd51 100644 --- a/src/app/[orgId]/settings/logs/action/page.tsx +++ b/src/app/[orgId]/settings/logs/action/page.tsx @@ -4,12 +4,13 @@ import { toast } from "@app/hooks/useToast"; import { useState, useRef, useEffect } from "react"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useParams, useRouter } from "next/navigation"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { LogDataTable } from "@app/components/LogDataTable"; import { ColumnDef } from "@tanstack/react-table"; import { DateTimeValue } from "@app/components/DateTimePicker"; import { Key, User } from "lucide-react"; +import { ColumnFilter } from "@app/components/ColumnFilter"; export default function GeneralPage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -18,10 +19,27 @@ export default function GeneralPage() { const t = useTranslations(); const { env } = useEnvContext(); const { orgId } = useParams(); + const searchParams = useSearchParams(); const [rows, setRows] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); const [isExporting, setIsExporting] = useState(false); + const [filterAttributes, setFilterAttributes] = useState<{ + actors: string[]; + actions: string[]; + }>({ + actors: [], + actions: [] + }); + + // Filter states - unified object for all filters + const [filters, setFilters] = useState<{ + action?: string; + actor?: string; + }>({ + action: searchParams.get("action") || undefined, + actor: searchParams.get("actor") || undefined + }); // Pagination state const [totalCount, setTotalCount] = useState(0); @@ -31,6 +49,20 @@ export default function GeneralPage() { // Set default date range to last 24 hours const getDefaultDateRange = () => { + // if the time is in the url params, use that instead + const startParam = searchParams.get("start"); + const endParam = searchParams.get("end"); + if (startParam && endParam) { + return { + startDate: { + date: new Date(startParam) + }, + endDate: { + date: new Date(endParam) + } + }; + } + const now = new Date(); const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); @@ -52,7 +84,12 @@ export default function GeneralPage() { // Trigger search with default values on component mount useEffect(() => { const defaultRange = getDefaultDateRange(); - queryDateTime(defaultRange.startDate, defaultRange.endDate); + queryDateTime( + defaultRange.startDate, + defaultRange.endDate, + 0, + pageSize + ); }, [orgId]); // Re-run if orgId changes const handleDateRangeChange = ( @@ -61,6 +98,12 @@ export default function GeneralPage() { ) => { setDateRange({ startDate, endDate }); setCurrentPage(0); // Reset to first page when filtering + // put the search params in the url for the time + updateUrlParamsForAllFilters({ + start: startDate.date?.toISOString() || "", + end: endDate.date?.toISOString() || "" + }); + queryDateTime(startDate, endDate, 0, pageSize); }; @@ -82,20 +125,74 @@ export default function GeneralPage() { queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize); }; + // Handle filter changes generically + const handleFilterChange = ( + filterType: keyof typeof filters, + value: string | undefined + ) => { + // Create new filters object with updated value + const newFilters = { + ...filters, + [filterType]: value + }; + + setFilters(newFilters); + setCurrentPage(0); // Reset to first page when filtering + + // Update URL params + updateUrlParamsForAllFilters(newFilters); + + // Trigger new query with updated filters (pass directly to avoid async state issues) + queryDateTime( + dateRange.startDate, + dateRange.endDate, + 0, + pageSize, + newFilters + ); + }; + + const updateUrlParamsForAllFilters = ( + newFilters: + | typeof filters + | { + start: string; + end: string; + } + ) => { + const params = new URLSearchParams(searchParams); + Object.entries(newFilters).forEach(([key, value]) => { + if (value) { + params.set(key, value); + } else { + params.delete(key); + } + }); + router.replace(`?${params.toString()}`, { scroll: false }); + }; + const queryDateTime = async ( startDate: DateTimeValue, endDate: DateTimeValue, page: number = currentPage, - size: number = pageSize + size: number = pageSize, + filtersParam?: { + action?: string; + actor?: string; + } ) => { console.log("Date range changed:", { startDate, endDate, page, size }); setIsLoading(true); try { + // Use the provided filters or fall back to current state + const activeFilters = filtersParam || filters; + // Convert the date/time values to API parameters let params: any = { limit: size, - offset: page * size + offset: page * size, + ...activeFilters }; if (startDate?.date) { @@ -133,6 +230,7 @@ export default function GeneralPage() { if (res.status === 200) { setRows(res.data.data.log || []); setTotalCount(res.data.data.pagination?.total || 0); + setFilterAttributes(res.data.data.filterAttributes); console.log("Fetched logs:", res.data); } } catch (error) { @@ -171,16 +269,21 @@ export default function GeneralPage() { const exportData = async () => { try { setIsExporting(true); + + // Prepare query params for export + let params: any = { + timeStart: dateRange.startDate?.date + ? new Date(dateRange.startDate.date).toISOString() + : undefined, + timeEnd: dateRange.endDate?.date + ? new Date(dateRange.endDate.date).toISOString() + : undefined, + ...filters + }; + const response = await api.get(`/org/${orgId}/logs/action/export`, { responseType: "blob", - params: { - timeStart: dateRange.startDate?.date - ? new Date(dateRange.startDate.date).toISOString() - : undefined, - timeEnd: dateRange.endDate?.date - ? new Date(dateRange.endDate.date).toISOString() - : undefined - } + params }); // Create a URL for the blob and trigger a download @@ -224,9 +327,27 @@ export default function GeneralPage() { { accessorKey: "action", header: ({ column }) => { - return t("action"); + return ( +
+ {t("action")} + ({ + label: + action.charAt(0).toUpperCase() + + action.slice(1), + value: action + }))} + selectedValue={filters.action} + onValueChange={(value) => + handleFilterChange("action", value) + } + // placeholder="" + searchPlaceholder="Search..." + emptyMessage="None found" + /> +
+ ); }, - // make the value capitalized cell: ({ row }) => { return ( @@ -239,7 +360,24 @@ export default function GeneralPage() { { accessorKey: "actor", header: ({ column }) => { - return t("actor"); + return ( +
+ {t("actor")} + ({ + value: actor, + label: actor + }))} + selectedValue={filters.actor} + onValueChange={(value) => + handleFilterChange("actor", value) + } + // placeholder="" + searchPlaceholder="Search..." + emptyMessage="None found" + /> +
+ ); }, cell: ({ row }) => { return ( @@ -276,7 +414,13 @@ export default function GeneralPage() {
Metadata:
-                            {row.metadata ? JSON.stringify(JSON.parse(row.metadata), null, 2) : "N/A"}
+                            {row.metadata
+                                ? JSON.stringify(
+                                      JSON.parse(row.metadata),
+                                      null,
+                                      2
+                                  )
+                                : "N/A"}
                         
diff --git a/src/app/[orgId]/settings/logs/request/page.tsx b/src/app/[orgId]/settings/logs/request/page.tsx index 50ae3740..f7731129 100644 --- a/src/app/[orgId]/settings/logs/request/page.tsx +++ b/src/app/[orgId]/settings/logs/request/page.tsx @@ -45,7 +45,7 @@ export default function GeneralPage() { resources: [], locations: [], hosts: [], - paths: [] + paths: [] }); // Filter states - unified object for all filters @@ -457,7 +457,7 @@ export default function GeneralPage() { {row.original.location ? ( - ({row.original.location}) + {row.original.location} ) : ( @@ -564,7 +564,7 @@ export default function GeneralPage() { /> ); - }, + } }, // { @@ -588,8 +588,7 @@ export default function GeneralPage() { { value: "PATCH", label: "PATCH" }, { value: "HEAD", label: "HEAD" }, { value: "OPTIONS", label: "OPTIONS" } - ] - } + ]} selectedValue={filters.method} onValueChange={(value) => handleFilterChange("method", value) @@ -600,7 +599,7 @@ export default function GeneralPage() { /> ); - }, + } }, { accessorKey: "reason", @@ -622,7 +621,10 @@ export default function GeneralPage() { { value: "202", label: t("resourceBlocked") }, { value: "203", label: t("droppedByRule") }, { value: "204", label: t("noSessions") }, - { value: "205", label: t("temporaryRequestToken") }, + { + value: "205", + label: t("temporaryRequestToken") + }, { value: "299", label: t("noMoreAuthMethods") } ]} selectedValue={filters.reason} @@ -712,14 +714,24 @@ export default function GeneralPage() {
Metadata:
-                            {row.metadata ? JSON.stringify(JSON.parse(row.metadata), null, 2) : "N/A"}
+                            {row.metadata
+                                ? JSON.stringify(
+                                      JSON.parse(row.metadata),
+                                      null,
+                                      2
+                                  )
+                                : "N/A"}
                         
{row.headers && (
Headers:
-                                {JSON.stringify(JSON.parse(row.headers), null, 2)}
+                                {JSON.stringify(
+                                    JSON.parse(row.headers),
+                                    null,
+                                    2
+                                )}
                             
)}