forked from administration/panel
feat: object notes
parent
6cfe0a2ffa
commit
03fffc9849
|
@ -24,6 +24,7 @@ import { User } from "revolt-api";
|
|||
import { decodeTime } from "ulid";
|
||||
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export default async function User({
|
||||
|
@ -43,6 +44,7 @@ export default async function User({
|
|||
{user && <UserCard user={user} subtitle={account.email} withLink />}
|
||||
<AccountActions account={account} user={user as User} />
|
||||
<EmailClassificationCard email={account.email} />
|
||||
<SafetyNotesCard objectId={account._id} type="account" />
|
||||
|
||||
<Separator />
|
||||
<Card>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ChannelCard } from "@/components/cards/ChannelCard";
|
||||
import { JsonCard } from "@/components/cards/JsonCard";
|
||||
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||
import { ServerCard } from "@/components/cards/ServerCard";
|
||||
import { UserCard } from "@/components/cards/UserCard";
|
||||
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
||||
|
@ -34,6 +35,8 @@ export default async function Message({ params }: { params: { id: string } }) {
|
|||
</Link>
|
||||
)}
|
||||
|
||||
<SafetyNotesCard objectId={channel._id} type="channel" />
|
||||
|
||||
{participants.length ? (
|
||||
<>
|
||||
<Separator />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ChannelCard } from "@/components/cards/ChannelCard";
|
||||
import { JsonCard } from "@/components/cards/JsonCard";
|
||||
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||
import { UserCard } from "@/components/cards/UserCard";
|
||||
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
|
@ -28,6 +29,8 @@ export default async function Message({
|
|||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<SafetyNotesCard objectId={message._id} type="message" />
|
||||
|
||||
{author && (
|
||||
<Link href={`/panel/inspect/user/${author!._id}`}>
|
||||
<UserCard user={author!} subtitle="Message Author" />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { JsonCard } from "@/components/cards/JsonCard";
|
||||
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||
import { ServerCard } from "@/components/cards/ServerCard";
|
||||
import { UserCard } from "@/components/cards/UserCard";
|
||||
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
||||
|
@ -21,6 +22,7 @@ export default async function Server({ params }: { params: { id: string } }) {
|
|||
<NavigationToolbar>Inspecting Server</NavigationToolbar>
|
||||
<ServerCard server={server} subtitle="Server" />
|
||||
<ServerActions server={server} />
|
||||
<SafetyNotesCard objectId={server._id} type="server" />
|
||||
{server.description && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { JsonCard } from "@/components/cards/JsonCard";
|
||||
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||
import { UserCard } from "@/components/cards/UserCard";
|
||||
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
||||
import { RecentMessages } from "@/components/pages/inspector/RecentMessages";
|
||||
|
@ -76,6 +77,7 @@ export default async function User({
|
|||
|
||||
<UserCard user={user} subtitle={user.status?.text ?? "No status set"} />
|
||||
<UserActions user={user} bot={bot as Bot} />
|
||||
<SafetyNotesCard objectId={user._id} type="user" />
|
||||
|
||||
{user.profile?.content && (
|
||||
<Card>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||
|
||||
export default function Home() {
|
||||
return <main>this is the admin panel ever</main>;
|
||||
return <main>
|
||||
<SafetyNotesCard objectId="home" type="global" title="Bulletin board" />
|
||||
</main>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import { toast } from "../ui/use-toast";
|
||||
import { SafetyNotes, fetchSafetyNote, updateSafetyNote } from "@/lib/db";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../ui/card";
|
||||
import { useSession } from "next-auth/react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export default function SafetyNotesCard({ objectId, type, title }: {
|
||||
objectId: string,
|
||||
type: SafetyNotes["_id"]["type"],
|
||||
title?: string
|
||||
}) {
|
||||
const session = useSession();
|
||||
const [draft, setDraft] = useState("");
|
||||
const [value, setValue] = useState<SafetyNotes | null>(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSafetyNote(objectId, type)
|
||||
.then((note) => {
|
||||
setDraft(note?.text || "");
|
||||
setValue(note);
|
||||
setReady(true);
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(String(e));
|
||||
});
|
||||
}, [objectId, type]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{title ?? type.charAt(0).toUpperCase() + type.slice(1) + " notes"}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
placeholder={
|
||||
error
|
||||
? error
|
||||
: ready
|
||||
? "Enter notes here... (save on unfocus)"
|
||||
: "Fetching notes..."
|
||||
}
|
||||
className="!min-h-[80px] max-h-[50vh]"
|
||||
disabled={!ready || error != null}
|
||||
value={ready ? draft : undefined} // not defaulting to undefined causes next to complain
|
||||
onChange={(e) => setDraft(e.currentTarget.value)}
|
||||
onBlur={async () => {
|
||||
if (draft === value?.text ?? "") return;
|
||||
|
||||
try {
|
||||
await updateSafetyNote(objectId, type, draft);
|
||||
setValue({
|
||||
_id: { id: objectId, type: type },
|
||||
edited_at: new Date(Date.now()),
|
||||
edited_by: session.data?.user?.email || "",
|
||||
text: draft,
|
||||
});
|
||||
toast({
|
||||
title: "Updated notes",
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Failed to update notes",
|
||||
description: String(err),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="-my-2">
|
||||
<CardDescription>
|
||||
{
|
||||
value
|
||||
? <>Last edited {dayjs(value.edited_at).fromNow()} by {value.edited_by}</>
|
||||
: <>No object note set</>
|
||||
}
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { getServerSession } from "next-auth";
|
||||
import { insertAuditLog } from "./db";
|
||||
import { SafetyNotes, insertAuditLog } from "./db";
|
||||
|
||||
type Permission =
|
||||
| "authifier"
|
||||
|
@ -49,7 +49,11 @@ type Permission =
|
|||
| "/relations"}`
|
||||
| `/create${"" | "/alert" | "/strike"}`
|
||||
| `/update${"" | "/badges"}`
|
||||
| `/action${"" | "/unsuspend" | "/suspend" | "/wipe" | "/ban" | "/wipe-profile"}`}`;
|
||||
| `/action${"" | "/unsuspend" | "/suspend" | "/wipe" | "/ban" | "/wipe-profile"}`}`
|
||||
| `safety_notes${
|
||||
| ""
|
||||
| `/fetch${"" | `/${SafetyNotes["_id"]["type"]}`}`
|
||||
| `/update${"" | `/${SafetyNotes["_id"]["type"]}`}`}`;
|
||||
|
||||
const PermissionSets = {
|
||||
// Admin
|
||||
|
@ -65,6 +69,7 @@ const PermissionSets = {
|
|||
"sessions",
|
||||
"servers",
|
||||
"users",
|
||||
"safety_notes",
|
||||
] as Permission[],
|
||||
|
||||
// View open reports
|
||||
|
@ -92,6 +97,12 @@ const PermissionSets = {
|
|||
|
||||
"bots/fetch/by-id",
|
||||
"bots/update/discoverability",
|
||||
|
||||
"safety_notes/fetch/global",
|
||||
"safety_notes/fetch/server",
|
||||
"safety_notes/fetch/user",
|
||||
"safety_notes/update/server",
|
||||
"safety_notes/update/user",
|
||||
] as Permission[],
|
||||
|
||||
// User support
|
||||
|
@ -112,6 +123,9 @@ const PermissionSets = {
|
|||
|
||||
"channels/update/invites",
|
||||
"channels/fetch/invites",
|
||||
|
||||
"safety_notes/fetch",
|
||||
"safety_notes/update",
|
||||
] as Permission[],
|
||||
|
||||
// Moderate users
|
||||
|
@ -147,6 +161,9 @@ const PermissionSets = {
|
|||
|
||||
"publish_message",
|
||||
"chat_message",
|
||||
|
||||
"safety_notes/fetch",
|
||||
"safety_notes/update",
|
||||
] as Permission[],
|
||||
};
|
||||
|
||||
|
|
37
lib/db.ts
37
lib/db.ts
|
@ -560,3 +560,40 @@ export async function fetchAuthifierEmailClassification(provider: string) {
|
|||
.collection<EmailClassification>("email_classification")
|
||||
.findOne({ _id: provider });
|
||||
}
|
||||
|
||||
export type SafetyNotes = {
|
||||
_id: { id: string, type: "message" | "channel" | "server" | "user" | "account" | "global" };
|
||||
text: string;
|
||||
edited_by: string;
|
||||
edited_at: Date;
|
||||
}
|
||||
|
||||
export async function fetchSafetyNote(objectId: string, type: SafetyNotes["_id"]["type"]) {
|
||||
await checkPermission(`safety_notes/fetch/${type}`, objectId);
|
||||
|
||||
return mongo()
|
||||
.db("revolt")
|
||||
.collection<SafetyNotes>("safety_notes")
|
||||
.findOne({ _id: { id: objectId, type: type } });
|
||||
}
|
||||
|
||||
export async function updateSafetyNote(objectId: string, type: SafetyNotes["_id"]["type"], note: string) {
|
||||
await checkPermission(`safety_notes/update/${type}`, objectId);
|
||||
|
||||
const session = await getServerSession();
|
||||
|
||||
return mongo()
|
||||
.db("revolt")
|
||||
.collection<SafetyNotes>("safety_notes")
|
||||
.updateOne(
|
||||
{ _id: { id: objectId, type: type } },
|
||||
{
|
||||
$set: {
|
||||
text: note,
|
||||
edited_at: new Date(Date.now()),
|
||||
edited_by: session?.user?.email ?? "",
|
||||
},
|
||||
},
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue