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)}>
+
+
+ ) : (
+
+
+ {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!,