Skip to content

Commit

Permalink
refactor: query audit logs from planetscale (#2181)
Browse files Browse the repository at this point in the history
* refactor: query audit logs from planetscale

* fix: sort logs

* [autofix.ci] apply automated fixes

* chore: remove csv export

* Update apps/dashboard/app/(app)/audit/[bucket]/page.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fmt: add comma

* ci: remove wrong lint command

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored and mcstepp committed Oct 8, 2024
1 parent daecfe6 commit 49f0141
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 78 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/autofix.ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
33 changes: 0 additions & 33 deletions apps/dashboard/app/(app)/audit/[bucket]/export-csv.tsx

This file was deleted.

119 changes: 82 additions & 37 deletions apps/dashboard/app/(app)/audit/[bucket]/page.tsx
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -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 (
<EmptyPlaceholder>
<EmptyPlaceholder.Icon>
<Box />
</EmptyPlaceholder.Icon>
<EmptyPlaceholder.Title>Bucket Not Found</EmptyPlaceholder.Title>
<EmptyPlaceholder.Description>
The specified audit log bucket does not exist or you do not have access to it.
</EmptyPlaceholder.Description>
</EmptyPlaceholder>
);
}

return (
<div>
Expand Down Expand Up @@ -118,7 +136,6 @@ export default async function AuditPage(props: Props) {
) : null}
<Suspense fallback={<Filter param="rootKeys" title="Root Keys" options={[]} />}>
<RootKeyFilter workspaceId={workspace.id} />
<ExportCsv data={logs.data} />
</Suspense>
{selectedEvents.length > 0 || selectedUsers.length > 0 || selectedRootKeys.length > 0 ? (
<Link href="/audit">
Expand All @@ -143,7 +160,23 @@ export default async function AuditPage(props: Props) {
}
>
<AuditLogTable
logs={logs}
logs={bucket.logs.map((l) => ({
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}
Expand All @@ -160,12 +193,28 @@ const AuditLogTable: React.FC<{
selectedUsers: string[];
selectedRootKeys: string[];
before?: number;
logs: { data: z.infer<typeof auditLogsDataSchema>[] };
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 (
<EmptyPlaceholder>
<EmptyPlaceholder.Icon>
Expand All @@ -190,7 +239,7 @@ const AuditLogTable: React.FC<{
);
}

const hasMoreLogs = logs.data.length >= 100;
const hasMoreLogs = logs.length >= 100;

function buildHref(override: Partial<Props["searchParams"]>): string {
const searchParams = new URLSearchParams();
Expand All @@ -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(
Expand All @@ -234,18 +281,16 @@ const AuditLogTable: React.FC<{
<TableHeader>
<TableRow>
<TableHead>Actor</TableHead>
<TableHead>Event</TableHead>
<TableHead>Location</TableHead>
<TableHead>Time</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{logs.data.map((l) => {
{logs.map((l) => {
const user = users[l.actor.id];
return (
<Row
key={l.auditLogId}
key={l.id}
user={
user
? {
Expand All @@ -260,8 +305,8 @@ const AuditLogTable: React.FC<{
time: l.time,
actor: l.actor,
event: l.event,
location: l.context.location,
resources: l.resources,
location: l.location,
targets: l.targets,
description: l.description,
}}
/>
Expand All @@ -271,7 +316,7 @@ const AuditLogTable: React.FC<{
</Table>

<div className="w-full mt-8">
<Link href={buildHref({ before: logs.data?.at(-1)?.time })} prefetch>
<Link href={buildHref({ before: logs.at(-1)?.time })} prefetch>
<Button
size="block"
disabled={!hasMoreLogs}
Expand Down
9 changes: 4 additions & 5 deletions apps/dashboard/app/(app)/audit/[bucket]/row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ type Props = {
event: string;
actor: {
id: string;
type: "key" | "user" | "system";
type: string;
name: string | null;
};
location: string | null;
description: string;
resources: {
targets: {
type: string;
id: string;
meta?: Record<string, string | number | boolean | null>;
}[];
};
user?: {
Expand Down Expand Up @@ -100,8 +99,8 @@ export const Row: React.FC<Props> = ({ auditLog, user }) => {
<TableCell colSpan={4}>
<Code className="text-xxs">
{JSON.stringify(
auditLog.resources.reduce((acc, r) => {
acc[r.type] = r.id;
auditLog.targets.reduce((acc, t) => {
acc[t.type] = t.id;
return acc;
}, {} as any),
null,
Expand Down
8 changes: 8 additions & 0 deletions internal/db/src/schema/audit_logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export const auditLogBucket = mysqlTable(
}),
);

export const auditLogBucketRelations = relations(auditLogBucket, ({ one, many }) => ({
workspace: one(workspaces, {
fields: [auditLogBucket.workspaceId],
references: [workspaces.id],
}),
logs: many(auditLog),
}));

export const auditLog = mysqlTable(
"audit_log",
{
Expand Down

0 comments on commit 49f0141

Please sign in to comment.