"use client"; import { ColumnFilter } from "@app/components/ColumnFilter"; import { DateTimeValue } from "@app/components/DateTimePicker"; import { LogDataTable } from "@app/components/LogDataTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Button } from "@app/components/ui/button"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; import { useTranslations } from "next-intl"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; import { logQueries } from "@app/lib/queries"; import { ColumnDef } from "@tanstack/react-table"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; import { ArrowUpRight, Key, Lock, Unlock, User } from "lucide-react"; import Link from "next/link"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useMemo, useState, useTransition } from "react"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { build } from "@server/build"; import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types"; export default function GeneralPage() { const router = useRouter(); const api = createApiClient(useEnvContext()); const t = useTranslations(); const { orgId } = useParams(); const searchParams = useSearchParams(); const [isExporting, startTransition] = useTransition(); const [currentPage, setCurrentPage] = useState(0); const [pageSize, setPageSize] = useStoredPageSize("request-audit-logs", 20); const [filters, setFilters] = useState<{ action?: string; resourceId?: string; host?: string; location?: string; actor?: string; method?: string; reason?: string; path?: string; }>({ action: searchParams.get("action") || undefined, host: searchParams.get("host") || undefined, resourceId: searchParams.get("resourceId") || undefined, location: searchParams.get("location") || undefined, actor: searchParams.get("actor") || undefined, method: searchParams.get("method") || undefined, reason: searchParams.get("reason") || undefined, path: searchParams.get("path") || undefined }); const getDefaultDateRange = () => { const startParam = searchParams.get("start"); const endParam = searchParams.get("end"); if (startParam && endParam) { return { startDate: { date: new Date(startParam) }, endDate: { date: new Date(endParam) } }; } return { startDate: { date: getSevenDaysAgo() }, endDate: { date: new Date() } }; }; const [dateRange, setDateRange] = useState<{ startDate: DateTimeValue; endDate: DateTimeValue; }>(getDefaultDateRange()); const queryFilters = useMemo(() => { let timeStart: string | undefined; let timeEnd: string | undefined; if (dateRange.startDate?.date) { const dt = new Date(dateRange.startDate.date); if (dateRange.startDate.time) { const [h, m, s] = dateRange.startDate.time .split(":") .map(Number); dt.setHours(h, m, s || 0); } timeStart = dt.toISOString(); } if (dateRange.endDate?.date) { const dt = new Date(dateRange.endDate.date); if (dateRange.endDate.time) { const [h, m, s] = dateRange.endDate.time.split(":").map(Number); dt.setHours(h, m, s || 0); } else { const now = new Date(); dt.setHours( now.getHours(), now.getMinutes(), now.getSeconds(), now.getMilliseconds() ); } timeEnd = dt.toISOString(); } return { timeStart, timeEnd, page: currentPage, pageSize, ...filters, resourceId: filters.resourceId ? Number(filters.resourceId) : undefined }; }, [dateRange, currentPage, pageSize, filters]); const { data, isFetching, isLoading, refetch } = useQuery({ ...logQueries.requests({ orgId: orgId as string, filters: queryFilters }), enabled: build !== "oss" }); const rows = isLoading ? generateSampleRequestLogs() : (data?.log ?? []); const totalCount = data?.pagination?.total ?? 0; const filterAttributes = data?.filterAttributes ?? { actors: [], resources: [], locations: [], hosts: [], paths: [] }; const handleDateRangeChange = ( startDate: DateTimeValue, endDate: DateTimeValue ) => { setDateRange({ startDate, endDate }); setCurrentPage(0); updateUrlParamsForAllFilters({ start: startDate.date?.toISOString() || "", end: endDate.date?.toISOString() || "" }); }; const handlePageChange = (newPage: number) => { setCurrentPage(newPage); }; const handlePageSizeChange = (newPageSize: number) => { setPageSize(newPageSize); setCurrentPage(0); }; const handleFilterChange = ( filterType: keyof typeof filters, value: string | undefined ) => { const newFilters = { ...filters, [filterType]: value }; setFilters(newFilters); setCurrentPage(0); updateUrlParamsForAllFilters(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 exportData = async () => { try { // Prepare query params for export const 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/request/export`, { responseType: "blob", params } ); // Create a URL for the blob and trigger a download const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement("a"); link.href = url; const epoch = Math.floor(Date.now() / 1000); link.setAttribute( "download", `request-audit-logs-${orgId}-${epoch}.csv` ); document.body.appendChild(link); link.click(); link.parentNode?.removeChild(link); } catch (error) { let apiErrorMessage: string | null = null; if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (data instanceof Blob && data.type === "application/json") { // Parse the Blob as JSON const text = await data.text(); const errorData = JSON.parse(text); apiErrorMessage = errorData.message; } } toast({ title: t("error"), description: apiErrorMessage ?? t("exportError"), variant: "destructive" }); } }; // 100 - Allowed by Rule // 101 - Allowed No Auth // 102 - Valid Access Token // 103 - Valid header auth // 104 - Valid Pincode // 105 - Valid Password // 106 - Valid email // 107 - Valid SSO // 108 - Connected Client // 201 - Resource Not Found // 202 - Resource Blocked // 203 - Dropped by Rule // 204 - No Sessions // 205 - Temporary Request Token // 299 - No More Auth Methods const reasonMap: any = { 100: t("allowedByRule"), 101: t("allowedNoAuth"), 102: t("validAccessToken"), 103: t("validHeaderAuth"), 104: t("validPincode"), 105: t("validPassword"), 106: t("validEmail"), 107: t("validSSO"), 108: t("connectedClient"), 201: t("resourceNotFound"), 202: t("resourceBlocked"), 203: t("droppedByRule"), 204: t("noSessions"), 205: t("temporaryRequestToken"), 299: t("noMoreAuthMethods") }; // resourceId: integer("resourceId"), // userAgent: text("userAgent"), // metadata: text("details"), // headers: text("headers"), // JSON blob // query: text("query"), // JSON blob // originalRequestURL: text("originalRequestURL"), // scheme: text("scheme"), const columns: ColumnDef[] = [ { accessorKey: "timestamp", header: ({ column }) => { return t("timestamp"); }, cell: ({ row }) => { return (
{new Date( row.original.timestamp * 1000 ).toLocaleString()}
); } }, { accessorKey: "action", header: ({ column }) => { return (
{t("action")} handleFilterChange("action", value) } // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" />
); }, cell: ({ row }) => { return ( {row.original.action ? <>Allowed : <>Denied} ); } }, { accessorKey: "ip", header: ({ column }) => { return t("ip"); } }, { accessorKey: "location", header: ({ column }) => { return (
{t("location")} ({ value: location, label: location }) )} selectedValue={filters.location} onValueChange={(value) => handleFilterChange("location", value) } // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" />
); }, cell: ({ row }) => { return ( {row.original.location ? ( {row.original.location} ) : ( - )} ); } }, { accessorKey: "resourceName", header: ({ column }) => { return (
{t("resource")} ({ value: res.id.toString(), label: res.name || "Unnamed Resource" }))} selectedValue={filters.resourceId} onValueChange={(value) => handleFilterChange("resourceId", value) } // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" />
); }, cell: ({ row }) => { return ( e.stopPropagation()} > ); } }, { accessorKey: "host", header: ({ column }) => { return (
{t("host")} ({ value: host, label: host }))} selectedValue={filters.host} onValueChange={(value) => handleFilterChange("host", value) } // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" />
); }, cell: ({ row }) => { return ( {row.original.tls ? ( ) : ( )} {row.original.host} ); } }, { accessorKey: "path", header: ({ column }) => { return (
{t("path")} ({ value: path, label: path }))} selectedValue={filters.path} onValueChange={(value) => handleFilterChange("path", value) } // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" />
); } }, // { // accessorKey: "scheme", // header: ({ column }) => { // return t("scheme"); // }, // }, { accessorKey: "method", header: ({ column }) => { return (
{t("method")} handleFilterChange("method", value) } // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" />
); } }, { accessorKey: "reason", header: ({ column }) => { return (
{t("reason")} handleFilterChange("reason", value) } // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" />
); }, cell: ({ row }) => { return ( {reasonMap[row.original.reason]} ); } }, { accessorKey: "actor", header: ({ column }) => { return (
{t("actor")} ({ value: actor, label: actor }))} selectedValue={filters.actor} onValueChange={(value) => handleFilterChange("actor", value) } // placeholder="" searchPlaceholder="Search..." emptyMessage="None found" />
); }, cell: ({ row }) => { return ( {row.original.actor ? ( <> {row.original.actorType == "user" ? ( ) : ( )} {row.original.actor} ) : ( <>- )} ); } } ]; const renderExpandedRow = (row: any) => { return (
{/*
User Agent:

{row.userAgent || "N/A"}

*/}
Original URL:

{row.originalRequestURL || "N/A"}

{/*
Scheme:

{row.scheme || "N/A"}

*/}
Metadata:
                            {row.metadata
                                ? JSON.stringify(
                                      JSON.parse(row.metadata),
                                      null,
                                      2
                                  )
                                : "N/A"}
                        
{row.headers && (
Headers:
                                {JSON.stringify(
                                    JSON.parse(row.headers),
                                    null,
                                    2
                                )}
                            
)} {row.query && (
Query Parameters:
                                {JSON.stringify(JSON.parse(row.query), null, 2)}
                            
)}
); }; return ( <> refetch()} isRefreshing={isFetching} onExport={() => startTransition(exportData)} isExporting={isExporting} onDateRangeChange={handleDateRangeChange} dateRange={{ start: dateRange.startDate, end: dateRange.endDate }} defaultSort={{ id: "timestamp", desc: true }} totalCount={totalCount} currentPage={currentPage} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange} isLoading={isLoading} pageSize={pageSize} // Row expansion props expandable={true} renderExpandedRow={renderExpandedRow} /> ); } function generateSampleRequestLogs(): QueryRequestAuditLogResponse["log"] { const methods = ["GET", "POST", "PUT", "DELETE", "PATCH"]; const paths = [ "/api/v1/users", "/dashboard", "/settings", "/health", "/metrics" ]; const hosts = ["app.example.com", "api.example.com", "admin.example.com"]; const locations = ["US", "DE", "GB", "FR", "JP", "CA", "AU"]; const allowedReasons = [100, 101, 102, 103, 104, 105, 106, 107, 108]; const deniedReasons = [201, 202, 203, 204, 205, 299]; const actors = [ "alice@example.com", "bob@example.com", "carol@example.com", null ]; const now = Math.floor(Date.now() / 1000); const sevenDaysAgo = now - 7 * 24 * 60 * 60; return Array.from({ length: 10 }, (_, i) => { const action = Math.random() > 0.3; const reason = action ? allowedReasons[Math.floor(Math.random() * allowedReasons.length)] : deniedReasons[Math.floor(Math.random() * deniedReasons.length)]; const actor = actors[Math.floor(Math.random() * actors.length)]; return { timestamp: Math.floor( sevenDaysAgo + Math.random() * (now - sevenDaysAgo) ), action, reason, orgId: "sample-org", actorType: actor ? "user" : null, actor, actorId: actor ? `user-${i}` : null, resourceId: Math.floor(Math.random() * 5) + 1, siteResourceId: null, resourceNiceId: `resource-${(i % 3) + 1}`, resourceName: `Resource ${(i % 3) + 1}`, ip: `${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, location: locations[Math.floor(Math.random() * locations.length)], userAgent: "Mozilla/5.0", metadata: null, headers: null, query: null, originalRequestURL: null, scheme: "https", host: hosts[Math.floor(Math.random() * hosts.length)], path: paths[Math.floor(Math.random() * paths.length)], method: methods[Math.floor(Math.random() * methods.length)], tls: true }; }); }