diff --git a/.github/workflows/autofix.ci.yaml b/.github/workflows/autofix.ci.yaml index 82df563d81..510498089e 100644 --- a/.github/workflows/autofix.ci.yaml +++ b/.github/workflows/autofix.ci.yaml @@ -35,6 +35,3 @@ jobs: run: npx mintlify@latest broken-links working-directory: apps/docs - - name: Lint engineering docs - run: npx mintlify@latest broken-links - working-directory: apps/engineering diff --git a/apps/dashboard/app/(app)/audit/[bucket]/export-csv.tsx b/apps/dashboard/app/(app)/audit/[bucket]/export-csv.tsx deleted file mode 100644 index 306301c839..0000000000 --- a/apps/dashboard/app/(app)/audit/[bucket]/export-csv.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import type { auditLogsDataSchema } from "@/lib/tinybird"; -import { download, generateCsv, mkConfig } from "export-to-csv"; -import type { z } from "zod"; - -export function ExportCsv({ data }: { data: z.infer[] }) { - function csvDownload(rows: z.infer[]) { - const csvConfig = mkConfig({ - fieldSeparator: ",", - filename: "unkey-audit-logs", // export file name (without .csv) - decimalSeparator: ".", - useKeysAsHeaders: true, - }); - const formatted = rows - .map((row) => ({ - ...row, - actorId: row.actor.id, - ip: row.context.location, - userAgent: row.context.userAgent, - resources: JSON.stringify(row.resources), - })) - .map(({ actor, context, resources, ...flattenedRow }) => flattenedRow); - const csv = generateCsv(csvConfig)(formatted); - download(csvConfig)(csv); - } - return ( - - ); -} diff --git a/apps/dashboard/app/(app)/audit/[bucket]/page.tsx b/apps/dashboard/app/(app)/audit/[bucket]/page.tsx index 90474c3d43..b5c746ecbe 100644 --- a/apps/dashboard/app/(app)/audit/[bucket]/page.tsx +++ b/apps/dashboard/app/(app)/audit/[bucket]/page.tsx @@ -1,27 +1,23 @@ +import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Loading } from "@/components/dashboard/loading"; import { PageHeader } from "@/components/dashboard/page-header"; +import { Button } from "@/components/ui/button"; +import { Table, TableBody, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; import { clerkClient } from "@clerk/nextjs"; import type { User } from "@clerk/nextjs/server"; -import { redirect } from "next/navigation"; - -import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; -import { Loading } from "@/components/dashboard/loading"; -import { Button } from "@/components/ui/button"; -import { Table, TableBody, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { type auditLogsDataSchema, getAuditLogs } from "@/lib/tinybird"; import { unkeyAuditLogEvents } from "@unkey/schema/src/auditlog"; import { Box, X } from "lucide-react"; import Link from "next/link"; +import { redirect } from "next/navigation"; import { parseAsArrayOf, parseAsString } from "nuqs/server"; import { Suspense } from "react"; -import type { z } from "zod"; import { BucketSelect } from "./bucket-select"; import { Filter } from "./filter"; import { Row } from "./row"; export const dynamic = "force-dynamic"; export const runtime = "edge"; -import { ExportCsv } from "./export-csv"; type Props = { params: { @@ -67,21 +63,43 @@ export default async function AuditPage(props: Props) { */ const retentionDays = workspace.features.auditLogRetentionDays ?? workspace.plan === "free" ? 30 : 90; + const retentionCutoffUnixMilli = Date.now() - retentionDays * 24 * 60 * 60 * 1000; + + const selectedActorIds = [...selectedRootKeys, ...selectedUsers]; - const logs = await getAuditLogs({ - workspaceId: workspace.id, - before: props.searchParams.before ? Number(props.searchParams.before) : undefined, - after: Date.now() - retentionDays * 24 * 60 * 60 * 1000, - bucket: props.params.bucket, - events: selectedEvents.length > 0 ? selectedEvents : undefined, - actorIds: - selectedUsers.length > 0 || selectedRootKeys.length > 0 - ? [...selectedUsers, ...selectedRootKeys] - : undefined, - }).catch((err) => { - console.error(err); - throw err; + const bucket = await db.query.auditLogBucket.findFirst({ + where: (table, { eq, and }) => + and(eq(table.workspaceId, workspace.id), eq(table.name, props.params.bucket)), + with: { + logs: { + where: (table, { and, inArray, gte }) => + and( + selectedEvents.length > 0 ? inArray(table.event, selectedEvents) : undefined, + gte(table.createdAt, retentionCutoffUnixMilli), + selectedActorIds.length > 0 ? inArray(table.actorId, selectedActorIds) : undefined, + ), + + with: { + targets: true, + }, + orderBy: (table, { asc }) => asc(table.id), + limit: 100, + }, + }, }); + if (!bucket) { + return ( + + + + + Bucket Not Found + + The specified audit log bucket does not exist or you do not have access to it. + + + ); + } return (
@@ -118,7 +136,6 @@ export default async function AuditPage(props: Props) { ) : null} }> - {selectedEvents.length > 0 || selectedUsers.length > 0 || selectedRootKeys.length > 0 ? ( @@ -143,7 +160,23 @@ export default async function AuditPage(props: Props) { } > ({ + id: l.id, + event: l.event, + time: l.time, + actor: { + id: l.actorId, + name: l.actorName, + type: l.actorType, + }, + location: l.remoteIp, + description: l.display, + targets: l.targets.map((t) => ({ + id: t.id, + type: t.type, + name: t.name, + })), + }))} before={props.searchParams.before ? Number(props.searchParams.before) : undefined} selectedEvents={selectedEvents} selectedUsers={selectedUsers} @@ -160,12 +193,28 @@ const AuditLogTable: React.FC<{ selectedUsers: string[]; selectedRootKeys: string[]; before?: number; - logs: { data: z.infer[] }; + logs: Array<{ + id: string; + event: string; + time: number; + actor: { + id: string; + type: string; + name: string | null; + }; + location: string | null; + description: string; + targets: Array<{ + id: string; + type: string; + name: string | null; + }>; + }>; }> = async ({ selectedEvents, selectedRootKeys, selectedUsers, before, logs }) => { const isFiltered = selectedEvents.length > 0 || selectedUsers.length > 0 || selectedRootKeys.length > 0 || before; - if (logs.data.length === 0) { + if (logs.length === 0) { return ( @@ -190,7 +239,7 @@ const AuditLogTable: React.FC<{ ); } - const hasMoreLogs = logs.data.length >= 100; + const hasMoreLogs = logs.length >= 100; function buildHref(override: Partial): string { const searchParams = new URLSearchParams(); @@ -213,9 +262,7 @@ const AuditLogTable: React.FC<{ return `/audit?${searchParams.toString()}`; } - const userIds = [ - ...new Set(logs.data.filter((l) => l.actor.type === "user").map((l) => l.actor.id)), - ]; + const userIds = [...new Set(logs.filter((l) => l.actor.type === "user").map((l) => l.actor.id))]; const users = ( await Promise.all(userIds.map((userId) => clerkClient.users.getUser(userId).catch(() => null))) ).reduce( @@ -234,18 +281,16 @@ const AuditLogTable: React.FC<{ Actor - Event - Location Time - {logs.data.map((l) => { + {logs.map((l) => { const user = users[l.actor.id]; return ( @@ -271,7 +316,7 @@ const AuditLogTable: React.FC<{
- +