diff --git a/app/panel/audit/page.tsx b/app/panel/audit/page.tsx index 2be5b43..6bd6140 100644 --- a/app/panel/audit/page.tsx +++ b/app/panel/audit/page.tsx @@ -1,7 +1,93 @@ -export default function AuditLog() { +import { AuditLogEntryCard } from "@/components/cards/AuditLogEntryCard"; +import { CollapsibleSection } from "@/components/common/CollapsibleSection"; +import { Permission } from "@/lib/accessPermissions"; +import { fetchAuditLogEvents } from "@/lib/actions"; +import { AuditLogEntry } from "@/lib/db"; + +const collapseGroups: { perms: Permission[], name: string }[] = [ + { + name: "user", + perms: [ + "users/fetch/by-id", + "users/fetch/memberships", + "users/fetch/notices", + "users/fetch/relations", + "users/fetch/strikes", + "reports/fetch/related/by-user", + "reports/fetch/related/against-user", + ], + }, + { + name: "account", + perms: [ + "accounts/fetch/by-id", + "accounts/fetch/by-email", + "sessions/fetch/by-account-id", + ], + }, + { + name: "server", + perms: [ + "servers/fetch/by-id", + ], + }, + { + name: "channel", + perms: [ + "channels/fetch/by-id", + "channels/fetch/by-server", + ] + }, + { + name: "report", + perms: [ + "reports/fetch/by-id", + "reports/fetch/snapshots/by-report", + ], + } +]; + +const getGroup = (permission: string) => collapseGroups.findIndex((list) => list.perms.find((p) => p.startsWith(permission))); + +export default async function AuditLog() { + const entries = await fetchAuditLogEvents(); + const items: (AuditLogEntry | AuditLogEntry[])[] = []; + let tmp: AuditLogEntry[] = []; + + for (const entry of entries) { + const group = getGroup(entry.permission); + + if (tmp.length && group == getGroup(tmp[0].permission)) { + tmp.push(entry); + } else { + if (tmp.length) { + items.push(tmp); + tmp = []; + } + + if (group != -1) tmp.push(entry); + else items.push(entry); + } + } + items.push(...tmp); + return (
- chronological event log for all actions + {items.map((entry) => Array.isArray(entry) + ? ( + +
+ {entry.map((item) => )} +
+
+ ) + : ( + + ) + )}
); } diff --git a/components/cards/AuditLogEntryCard.tsx b/components/cards/AuditLogEntryCard.tsx new file mode 100644 index 0000000..9ea3e89 --- /dev/null +++ b/components/cards/AuditLogEntryCard.tsx @@ -0,0 +1,149 @@ +import { AuditLogEntry, fetchChannelById, fetchReportById, fetchServerById, fetchUserById, fetchUsersById } from "@/lib/db"; +import { Card, CardContent, CardDescription, CardHeader } from "../ui/card"; +import { UserCard } from "./UserCard"; +import { User } from "revolt-api"; +import Link from "next/link"; +import { Permission } from "@/lib/accessPermissions"; +import { ServerCard } from "./ServerCard"; +import { ChannelCard } from "./ChannelCard"; +import { ReportCard } from "./ReportCard"; +import { decodeTime } from "ulid"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; + +dayjs.extend(relativeTime); + +export async function AuditLogEntryCard({ log }: { log: AuditLogEntry }) { + const perm = log.permission as Permission; + + const ContextCard = async () => { + const fallback = log.context ? ( + + +
+            {JSON.stringify(log.context, null, 4)}
+          
+
+
+ ) : null; + + try { + // Users + if (perm.startsWith("users/action") + || perm.startsWith("users/create") + || perm == "users/fetch/by-id" + || perm == "users/fetch/notices" + || perm == "users/fetch/memberships" + || perm == "users/fetch/relations" + || perm == "users/fetch/strikes" + || perm == "reports/fetch/related/by-user" + || perm == "reports/fetch/related/against-user" + || perm == "reports/update/bulk-close/by-user" + ) { + let users: User[] = await fetchUsersById(Array.isArray(log.context) ? log.context : [log.context]); + if (!users.length) return fallback; + + return ( + <> + {users.map((user: User) => ( + + + + ))} + + ); + } + + // Accounts + if (perm == "accounts/fetch/by-id" + || perm.startsWith("accounts/deletion") + || perm == "accounts/disable" + || perm == "accounts/restore" + || perm == "accounts/update" + || perm == "sessions/fetch/by-account-id" + ) { + const user = await fetchUserById(log.context); + + if (!user) return fallback; + return ( + + + + ); + } + + // Servers + if (perm == "servers/fetch/by-id" + || perm.startsWith("servers/update") + || perm == "channels/fetch/by-server" + ) { + const server = await fetchServerById(log.context); + + if (!server) return fallback; + return ( + + + + ) + } + + // Channels + if (perm == "channels/fetch/by-id" + || perm.startsWith("channels/update") + ) { + const channel = await fetchChannelById(log.context); + + if (!channel) return fallback; + return ( + + + + ); + } + + // Reports + if (perm == "reports/fetch/by-id" + || perm == "reports/fetch/snapshots/by-report" + || perm == "reports/update/notes" + || perm == "reports/update/reject" + || perm == "reports/update/reopen" + || perm == "reports/update/resolve") { + const report = await fetchReportById(log.context); + + if (!report) return fallback; + return ( + + + + ) + } + + return fallback; + } catch(e) { + return fallback; + } + } + + return ( + + + + {log.moderator} · {log._id} · {dayjs(decodeTime(log._id)).fromNow()} + + {log.permission} + + + + {log.args != null && ( + + +
+                {JSON.stringify(log.args, null, 2)}
+              
+
+
+ )} +
+
+ ); +} diff --git a/components/common/CollapsibleSection.tsx b/components/common/CollapsibleSection.tsx new file mode 100644 index 0000000..be845d0 --- /dev/null +++ b/components/common/CollapsibleSection.tsx @@ -0,0 +1,23 @@ +"use client" + +import { useState } from "react" +import { Button } from "../ui/button"; + +export function CollapsibleSection({ children, text }: { children: React.ReactNode, text: string }) { + const [collapsed, setCollapsed] = useState(true); + + return collapsed ? ( +
setCollapsed(false)}> +
+
+ {text} +
+
+
+ ) : ( +
+ + {children} +
+ ); +} diff --git a/components/common/NavigationLinks.tsx b/components/common/NavigationLinks.tsx index 1a147c9..6fd3a94 100644 --- a/components/common/NavigationLinks.tsx +++ b/components/common/NavigationLinks.tsx @@ -52,18 +52,18 @@ export function NavigationLinks() { > - {/* - - + {/* + + ("safety_audit") + .find({}) +// .sort({ _id: -1 }) + .limit(100) + .toArray(); +} diff --git a/lib/db.ts b/lib/db.ts index f895f39..a38bd7c 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -15,7 +15,7 @@ import type { } from "revolt-api"; import { ulid } from "ulid"; import { publishMessage } from "./redis"; -import { checkPermission, hasPermissionFromSession } from "./accessPermissions"; +import { Permission, checkPermission, hasPermissionFromSession } from "./accessPermissions"; import { PLATFORM_MOD_ID } from "./constants"; import { getServerSession } from "next-auth"; @@ -31,6 +31,14 @@ function mongo() { export default mongo; +export type AuditLogEntry = { + _id: string; + moderator: string; + permission: Permission | string; + context: any; + args: any; +}; + export async function insertAuditLog( permission: string, context: any, @@ -41,13 +49,7 @@ export async function insertAuditLog( await mongo() .db("revolt") - .collection<{ - _id: string; - moderator: string; - permission: string; - context: any; - args: any; - }>("safety_audit") + .collection("safety_audit") .insertOne({ _id: ulid(), moderator: session!.user!.email!,