forked from administration/panel
feat: object notes
parent
7360333523
commit
915459f955
|
@ -24,6 +24,7 @@ import { User } from "revolt-api";
|
||||||
import { decodeTime } from "ulid";
|
import { decodeTime } from "ulid";
|
||||||
|
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
export default async function User({
|
export default async function User({
|
||||||
|
@ -43,6 +44,7 @@ export default async function User({
|
||||||
{user && <UserCard user={user} subtitle={account.email} withLink />}
|
{user && <UserCard user={user} subtitle={account.email} withLink />}
|
||||||
<AccountActions account={account} user={user as User} />
|
<AccountActions account={account} user={user as User} />
|
||||||
<EmailClassificationCard email={account.email} />
|
<EmailClassificationCard email={account.email} />
|
||||||
|
<SafetyNotesCard objectId={account._id} type="account" />
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
<Card>
|
<Card>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ChannelCard } from "@/components/cards/ChannelCard";
|
import { ChannelCard } from "@/components/cards/ChannelCard";
|
||||||
import { JsonCard } from "@/components/cards/JsonCard";
|
import { JsonCard } from "@/components/cards/JsonCard";
|
||||||
|
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||||
import { ServerCard } from "@/components/cards/ServerCard";
|
import { ServerCard } from "@/components/cards/ServerCard";
|
||||||
import { UserCard } from "@/components/cards/UserCard";
|
import { UserCard } from "@/components/cards/UserCard";
|
||||||
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
||||||
|
@ -34,6 +35,8 @@ export default async function Message({ params }: { params: { id: string } }) {
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SafetyNotesCard objectId={channel._id} type="channel" />
|
||||||
|
|
||||||
{participants.length ? (
|
{participants.length ? (
|
||||||
<>
|
<>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ChannelCard } from "@/components/cards/ChannelCard";
|
import { ChannelCard } from "@/components/cards/ChannelCard";
|
||||||
import { JsonCard } from "@/components/cards/JsonCard";
|
import { JsonCard } from "@/components/cards/JsonCard";
|
||||||
|
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||||
import { UserCard } from "@/components/cards/UserCard";
|
import { UserCard } from "@/components/cards/UserCard";
|
||||||
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
||||||
import { Card, CardHeader } from "@/components/ui/card";
|
import { Card, CardHeader } from "@/components/ui/card";
|
||||||
|
@ -28,6 +29,8 @@ export default async function Message({
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<SafetyNotesCard objectId={message._id} type="message" />
|
||||||
|
|
||||||
{author && (
|
{author && (
|
||||||
<Link href={`/panel/inspect/user/${author!._id}`}>
|
<Link href={`/panel/inspect/user/${author!._id}`}>
|
||||||
<UserCard user={author!} subtitle="Message Author" />
|
<UserCard user={author!} subtitle="Message Author" />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { JsonCard } from "@/components/cards/JsonCard";
|
import { JsonCard } from "@/components/cards/JsonCard";
|
||||||
|
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||||
import { ServerCard } from "@/components/cards/ServerCard";
|
import { ServerCard } from "@/components/cards/ServerCard";
|
||||||
import { UserCard } from "@/components/cards/UserCard";
|
import { UserCard } from "@/components/cards/UserCard";
|
||||||
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
||||||
|
@ -21,6 +22,7 @@ export default async function Server({ params }: { params: { id: string } }) {
|
||||||
<NavigationToolbar>Inspecting Server</NavigationToolbar>
|
<NavigationToolbar>Inspecting Server</NavigationToolbar>
|
||||||
<ServerCard server={server} subtitle="Server" />
|
<ServerCard server={server} subtitle="Server" />
|
||||||
<ServerActions server={server} />
|
<ServerActions server={server} />
|
||||||
|
<SafetyNotesCard objectId={server._id} type="server" />
|
||||||
{server.description && (
|
{server.description && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { JsonCard } from "@/components/cards/JsonCard";
|
import { JsonCard } from "@/components/cards/JsonCard";
|
||||||
|
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||||
import { UserCard } from "@/components/cards/UserCard";
|
import { UserCard } from "@/components/cards/UserCard";
|
||||||
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
||||||
import { RecentMessages } from "@/components/pages/inspector/RecentMessages";
|
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"} />
|
<UserCard user={user} subtitle={user.status?.text ?? "No status set"} />
|
||||||
<UserActions user={user} bot={bot as Bot} />
|
<UserActions user={user} bot={bot as Bot} />
|
||||||
|
<SafetyNotesCard objectId={user._id} type="user" />
|
||||||
|
|
||||||
{user.profile?.content && (
|
{user.profile?.content && (
|
||||||
<Card>
|
<Card>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import SafetyNotesCard from "@/components/cards/SafetyNotesCard";
|
||||||
|
|
||||||
export default function Home() {
|
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 { getServerSession } from "next-auth";
|
||||||
import { insertAuditLog } from "./db";
|
import { SafetyNotes, insertAuditLog } from "./db";
|
||||||
|
|
||||||
type Permission =
|
type Permission =
|
||||||
| "authifier"
|
| "authifier"
|
||||||
|
@ -49,7 +49,11 @@ type Permission =
|
||||||
| "/relations"}`
|
| "/relations"}`
|
||||||
| `/create${"" | "/alert" | "/strike"}`
|
| `/create${"" | "/alert" | "/strike"}`
|
||||||
| `/update${"" | "/badges"}`
|
| `/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 = {
|
const PermissionSets = {
|
||||||
// Admin
|
// Admin
|
||||||
|
@ -65,6 +69,7 @@ const PermissionSets = {
|
||||||
"sessions",
|
"sessions",
|
||||||
"servers",
|
"servers",
|
||||||
"users",
|
"users",
|
||||||
|
"safety_notes",
|
||||||
] as Permission[],
|
] as Permission[],
|
||||||
|
|
||||||
// View open reports
|
// View open reports
|
||||||
|
@ -92,6 +97,12 @@ const PermissionSets = {
|
||||||
|
|
||||||
"bots/fetch/by-id",
|
"bots/fetch/by-id",
|
||||||
"bots/update/discoverability",
|
"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[],
|
] as Permission[],
|
||||||
|
|
||||||
// User support
|
// User support
|
||||||
|
@ -112,6 +123,9 @@ const PermissionSets = {
|
||||||
|
|
||||||
"channels/update/invites",
|
"channels/update/invites",
|
||||||
"channels/fetch/invites",
|
"channels/fetch/invites",
|
||||||
|
|
||||||
|
"safety_notes/fetch",
|
||||||
|
"safety_notes/update",
|
||||||
] as Permission[],
|
] as Permission[],
|
||||||
|
|
||||||
// Moderate users
|
// Moderate users
|
||||||
|
@ -147,6 +161,9 @@ const PermissionSets = {
|
||||||
|
|
||||||
"publish_message",
|
"publish_message",
|
||||||
"chat_message",
|
"chat_message",
|
||||||
|
|
||||||
|
"safety_notes/fetch",
|
||||||
|
"safety_notes/update",
|
||||||
] as Permission[],
|
] as Permission[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
37
lib/db.ts
37
lib/db.ts
|
@ -560,3 +560,40 @@ export async function fetchAuthifierEmailClassification(provider: string) {
|
||||||
.collection<EmailClassification>("email_classification")
|
.collection<EmailClassification>("email_classification")
|
||||||
.findOne({ _id: provider });
|
.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