1
0
Fork 0

feat: audit log ui elements

feat/audit-log
Lea 2023-11-19 23:13:11 +01:00
parent cdb05a5af7
commit d3bf9411c5
Signed by: lea
GPG Key ID: 1BAFFE8347019C42
7 changed files with 294 additions and 18 deletions

View File

@ -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 (
<div className="flex flex-col gap-2">
chronological event log for all actions
{items.map((entry) => Array.isArray(entry)
? (
<CollapsibleSection
text={`${entry.length} ${collapseGroups[getGroup(entry[0].permission)].name} fetch event${entry.length == 1 ? "" : "s"}`}
key={entries.indexOf(entry as any)}
>
<div className="flex flex-col gap-2">
{entry.map((item) => <AuditLogEntryCard key={item._id} log={item} />)}
</div>
</CollapsibleSection>
)
: (
<AuditLogEntryCard key={entry._id} log={entry} />
)
)}
</div>
);
}

View File

@ -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 ? (
<Card>
<CardHeader className="p-3">
<pre>
<code>{JSON.stringify(log.context, null, 4)}</code>
</pre>
</CardHeader>
</Card>
) : 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) => (
<Link href={`/panel/inspect/user/${log.context}`} key={user._id}>
<UserCard subtitle={user._id} user={user as User} />
</Link>
))}
</>
);
}
// 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 (
<Link href={`/panel/inspect/account/${log.context}`}>
<UserCard subtitle={user._id} user={user as User} />
</Link>
);
}
// 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 (
<Link href={`/panel/inspect/server/${server._id}`}>
<ServerCard server={server} subtitle={server._id} />
</Link>
)
}
// Channels
if (perm == "channels/fetch/by-id"
|| perm.startsWith("channels/update")
) {
const channel = await fetchChannelById(log.context);
if (!channel) return fallback;
return (
<Link href={`/panel/inspect/channel/${channel._id}`}>
<ChannelCard channel={channel} subtitle={channel._id} />
</Link>
);
}
// 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 (
<Link href={`/panel/reports/${report._id}`}>
<ReportCard report={report} />
</Link>
)
}
return fallback;
} catch(e) {
return fallback;
}
}
return (
<Card>
<CardHeader>
<CardDescription>
{log.moderator} &middot; {log._id} &middot; {dayjs(decodeTime(log._id)).fromNow()}
</CardDescription>
<code className="p-1">{log.permission}</code>
<ContextCard />
{log.args != null && (
<Card>
<CardHeader className="p-3">
<pre>
<code>{JSON.stringify(log.args, null, 2)}</code>
</pre>
</CardHeader>
</Card>
)}
</CardHeader>
</Card>
);
}

View File

@ -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 ? (
<div className="cursor-pointer select-none" onClick={() => setCollapsed(false)}>
<div className="flex flex-row items-center">
<div className="flex flex-grow h-[1px] bg-slate-200 mx-4" />
<span className="flex-shrink text-slate-500">{text}</span>
<div className="flex flex-grow h-[1px] bg-slate-200 mx-4" />
</div>
</div>
) : (
<div className="p-4 border-solid border-slate-200 border-[1px] rounded-lg bg-slate-50">
<Button variant="outline" className="mb-4" onClick={() => setCollapsed(true)}>Close</Button>
{children}
</div>
);
}

View File

@ -52,18 +52,18 @@ export function NavigationLinks() {
>
<Bomb className="h-4 w-4" />
</Link>
{/*<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/discover"
>
<Globe2 className="h-4 w-4" />
</Link>
<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/audit"
>
<ScrollText className="h-4 w-4" />
</Link>
{/*<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/discover"
>
<Globe2 className="h-4 w-4" />
</Link>
<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/activity"

View File

@ -1,7 +1,7 @@
import { getServerSession } from "next-auth";
import { SafetyNotes, insertAuditLog } from "./db";
type Permission =
export type Permission =
| `authifier${
| ""
| `/classification${
@ -81,7 +81,8 @@ type Permission =
| ""
| `/fetch${"" | `/${SafetyNotes["_id"]["type"]}`}`
| `/update${"" | `/${SafetyNotes["_id"]["type"]}`}`}`
| `backup${"" | `/fetch${"" | "/by-name"}`}`;
| `backup${"" | `/fetch${"" | "/by-name"}`}`
| `audit${"" | `/fetch`}`;
const PermissionSets = {
// Admin
@ -210,6 +211,8 @@ const PermissionSets = {
"safety_notes/fetch",
"safety_notes/update",
"audit/fetch",
] as Permission[],
"authifier": [

View File

@ -4,6 +4,7 @@ import { readFile, readdir, writeFile } from "fs/promises";
import { PLATFORM_MOD_ID, RESTRICT_ACCESS_LIST } from "./constants";
import mongo, {
Account,
AuditLogEntry,
ChannelInvite,
EmailClassification,
createDM,
@ -1053,3 +1054,15 @@ export async function fetchUsersByUsername(username: string) {
})
.toArray();
}
export async function fetchAuditLogEvents() {
await checkPermission("audit/fetch", {});
return await mongo()
.db("revolt")
.collection<AuditLogEntry>("safety_audit")
.find({})
// .sort({ _id: -1 })
.limit(100)
.toArray();
}

View File

@ -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<AuditLogEntry>("safety_audit")
.insertOne({
_id: ulid(),
moderator: session!.user!.email!,