forked from administration/panel
Compare commits
1 Commits
main
...
feat/audit
Author | SHA1 | Date |
---|---|---|
|
d3bf9411c5 |
|
@ -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 (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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} · {log._id} · {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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -52,18 +52,18 @@ export function NavigationLinks() {
|
||||||
>
|
>
|
||||||
<Bomb className="h-4 w-4" />
|
<Bomb className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
{/*<Link
|
|
||||||
className={buttonVariants({ variant: "outline", size: "icon" })}
|
|
||||||
href="/panel/discover"
|
|
||||||
>
|
|
||||||
<Globe2 className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
<Link
|
<Link
|
||||||
className={buttonVariants({ variant: "outline", size: "icon" })}
|
className={buttonVariants({ variant: "outline", size: "icon" })}
|
||||||
href="/panel/audit"
|
href="/panel/audit"
|
||||||
>
|
>
|
||||||
<ScrollText className="h-4 w-4" />
|
<ScrollText className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
|
{/*<Link
|
||||||
|
className={buttonVariants({ variant: "outline", size: "icon" })}
|
||||||
|
href="/panel/discover"
|
||||||
|
>
|
||||||
|
<Globe2 className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
className={buttonVariants({ variant: "outline", size: "icon" })}
|
className={buttonVariants({ variant: "outline", size: "icon" })}
|
||||||
href="/panel/activity"
|
href="/panel/activity"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { SafetyNotes, insertAuditLog } from "./db";
|
import { SafetyNotes, insertAuditLog } from "./db";
|
||||||
|
|
||||||
type Permission =
|
export type Permission =
|
||||||
| `authifier${
|
| `authifier${
|
||||||
| ""
|
| ""
|
||||||
| `/classification${
|
| `/classification${
|
||||||
|
@ -81,7 +81,8 @@ type Permission =
|
||||||
| ""
|
| ""
|
||||||
| `/fetch${"" | `/${SafetyNotes["_id"]["type"]}`}`
|
| `/fetch${"" | `/${SafetyNotes["_id"]["type"]}`}`
|
||||||
| `/update${"" | `/${SafetyNotes["_id"]["type"]}`}`}`
|
| `/update${"" | `/${SafetyNotes["_id"]["type"]}`}`}`
|
||||||
| `backup${"" | `/fetch${"" | "/by-name"}`}`;
|
| `backup${"" | `/fetch${"" | "/by-name"}`}`
|
||||||
|
| `audit${"" | `/fetch`}`;
|
||||||
|
|
||||||
const PermissionSets = {
|
const PermissionSets = {
|
||||||
// Admin
|
// Admin
|
||||||
|
@ -210,6 +211,8 @@ const PermissionSets = {
|
||||||
|
|
||||||
"safety_notes/fetch",
|
"safety_notes/fetch",
|
||||||
"safety_notes/update",
|
"safety_notes/update",
|
||||||
|
|
||||||
|
"audit/fetch",
|
||||||
] as Permission[],
|
] as Permission[],
|
||||||
|
|
||||||
"authifier": [
|
"authifier": [
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { readFile, readdir, writeFile } from "fs/promises";
|
||||||
import { PLATFORM_MOD_ID, RESTRICT_ACCESS_LIST } from "./constants";
|
import { PLATFORM_MOD_ID, RESTRICT_ACCESS_LIST } from "./constants";
|
||||||
import mongo, {
|
import mongo, {
|
||||||
Account,
|
Account,
|
||||||
|
AuditLogEntry,
|
||||||
ChannelInvite,
|
ChannelInvite,
|
||||||
EmailClassification,
|
EmailClassification,
|
||||||
createDM,
|
createDM,
|
||||||
|
@ -1053,3 +1054,15 @@ export async function fetchUsersByUsername(username: string) {
|
||||||
})
|
})
|
||||||
.toArray();
|
.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();
|
||||||
|
}
|
||||||
|
|
18
lib/db.ts
18
lib/db.ts
|
@ -15,7 +15,7 @@ import type {
|
||||||
} from "revolt-api";
|
} from "revolt-api";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
import { publishMessage } from "./redis";
|
import { publishMessage } from "./redis";
|
||||||
import { checkPermission, hasPermissionFromSession } from "./accessPermissions";
|
import { Permission, checkPermission, hasPermissionFromSession } from "./accessPermissions";
|
||||||
import { PLATFORM_MOD_ID } from "./constants";
|
import { PLATFORM_MOD_ID } from "./constants";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
|
|
||||||
|
@ -31,6 +31,14 @@ function mongo() {
|
||||||
|
|
||||||
export default mongo;
|
export default mongo;
|
||||||
|
|
||||||
|
export type AuditLogEntry = {
|
||||||
|
_id: string;
|
||||||
|
moderator: string;
|
||||||
|
permission: Permission | string;
|
||||||
|
context: any;
|
||||||
|
args: any;
|
||||||
|
};
|
||||||
|
|
||||||
export async function insertAuditLog(
|
export async function insertAuditLog(
|
||||||
permission: string,
|
permission: string,
|
||||||
context: any,
|
context: any,
|
||||||
|
@ -41,13 +49,7 @@ export async function insertAuditLog(
|
||||||
|
|
||||||
await mongo()
|
await mongo()
|
||||||
.db("revolt")
|
.db("revolt")
|
||||||
.collection<{
|
.collection<AuditLogEntry>("safety_audit")
|
||||||
_id: string;
|
|
||||||
moderator: string;
|
|
||||||
permission: string;
|
|
||||||
context: any;
|
|
||||||
args: any;
|
|
||||||
}>("safety_audit")
|
|
||||||
.insertOne({
|
.insertOne({
|
||||||
_id: ulid(),
|
_id: ulid(),
|
||||||
moderator: session!.user!.email!,
|
moderator: session!.user!.email!,
|
||||||
|
|
Loading…
Reference in New Issue