From ea39b687035ef41d388992bc0d047636c8c9ee1d Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 30 Jul 2023 21:40:19 +0100 Subject: [PATCH] feat: further work on more narrow permissions --- app/panel/inspect/user/[id]/page.tsx | 44 +++---- app/panel/reports/[id]/page.tsx | 4 +- components/common/ListCompactor.tsx | 7 +- components/pages/inspector/RecentMessages.tsx | 43 ++++-- .../inspector/RelevantModerationNotices.tsx | 14 +- lib/accessPermissions.ts | 41 +++++- lib/db.ts | 122 +++++++++++++++++- 7 files changed, 210 insertions(+), 65 deletions(-) diff --git a/app/panel/inspect/user/[id]/page.tsx b/app/panel/inspect/user/[id]/page.tsx index 5d3ccdb..52a3f6a 100644 --- a/app/panel/inspect/user/[id]/page.tsx +++ b/app/panel/inspect/user/[id]/page.tsx @@ -8,13 +8,14 @@ import { RelevantReports } from "@/components/pages/inspector/RelevantReports"; import { UserActions } from "@/components/pages/inspector/UserActions"; import { Card, CardHeader } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; -import { PLATFORM_MOD_ID } from "@/lib/constants"; import { fetchBotById, fetchBotsByUser, fetchMembershipsByUser, - fetchMessages, + fetchNoticesByUser, fetchReports, + fetchReportsAgainstUser, + fetchReportsByUser, fetchServers, fetchSnapshots, fetchStrikesByUser, @@ -37,17 +38,14 @@ export default async function User({ const bot = user.bot ? await fetchBotById(user._id) : undefined; const botOwner = user.bot ? await fetchUserById(user.bot.owner) : undefined; - // Fetch strikes + // Fetch strikes and moderation alerts const strikes = await fetchStrikesByUser(user._id); - - // Fetch moderation alerts - const dm = await findDM(PLATFORM_MOD_ID, user._id); - const notices = dm ? await fetchMessages({ channel: dm._id }) : []; + const notices = await fetchNoticesByUser(user._id); // Fetch friends and bots - const botIds = await fetchBotsByUser(user._id).then((bots) => - bots.map((bot) => bot._id) - ); + const botIds = await fetchBotsByUser(user._id) + .then((bots) => bots.map((bot) => bot._id)) + .catch(() => []); const relevantUsers = await fetchUsersById([ ...botIds, @@ -59,30 +57,18 @@ export default async function User({ relevantUsers.sort((a) => (a.bot ? -1 : 0)); // Fetch server memberships - const serverMemberships = await fetchMembershipsByUser(user._id); + const serverMemberships = await fetchMembershipsByUser(user._id).catch( + () => [] + ); const servers = await fetchServers({ _id: { $in: serverMemberships.map((member) => member._id.server), }, - }); + }).catch(() => []); // Fetch reports - const reportsByUser = await fetchReports({ - author_id: user._id, - }); - - const reportIdsInvolvingUser = await fetchSnapshots({ - // TODO: slow query - $or: [{ "content._id": user._id }, { "content.author": user._id }], - }).then((snapshots) => [ - ...new Set(snapshots.map((snapshot) => snapshot.report_id)), - ]); - - const reportsAgainstUser = await fetchReports({ - _id: { - $in: reportIdsInvolvingUser, - }, - }); + const reportsByUser = await fetchReportsByUser(user._id); + const reportsAgainstUser = await fetchReportsAgainstUser(user._id); return (
@@ -123,7 +109,7 @@ export default async function User({ - + diff --git a/app/panel/reports/[id]/page.tsx b/app/panel/reports/[id]/page.tsx index 3aeae2e..da81b1a 100644 --- a/app/panel/reports/[id]/page.tsx +++ b/app/panel/reports/[id]/page.tsx @@ -8,7 +8,7 @@ import { NavigationToolbar } from "@/components/common/NavigationToolbar"; import { ReportActions } from "@/components/pages/inspector/ReportActions"; import { Separator } from "@/components/ui/separator"; import { - fetchRelatedReports, + fetchRelatedReportsByContent, fetchReportById, fetchSnapshotsByReport, fetchUserById, @@ -28,7 +28,7 @@ export default async function Reports({ params }: { params: { id: string } }) { snapshots .map((snapshot) => snapshot._id) .map((contentId) => - fetchRelatedReports(contentId).then((reports) => + fetchRelatedReportsByContent(contentId).then((reports) => reports.filter((entry) => entry._id !== report._id) ) ) diff --git a/components/common/ListCompactor.tsx b/components/common/ListCompactor.tsx index 286dccb..224108f 100644 --- a/components/common/ListCompactor.tsx +++ b/components/common/ListCompactor.tsx @@ -15,7 +15,12 @@ export function ListCompactor({ return ( <> {data.length === 0 && ( -

Empty List

+ <> +

Empty List

+

+ You may be lacking permissions. +

+ )} {data.slice(0, limit).map((item, index) => ( diff --git a/components/pages/inspector/RecentMessages.tsx b/components/pages/inspector/RecentMessages.tsx index 08053b5..a8fc505 100644 --- a/components/pages/inspector/RecentMessages.tsx +++ b/components/pages/inspector/RecentMessages.tsx @@ -7,23 +7,32 @@ import { CardHeader, CardTitle, } from "../../ui/card"; -import { fetchMessages, fetchUsersById } from "@/lib/db"; +import { fetchMessages, fetchMessagesByUser, fetchUsersById } from "@/lib/db"; import { CompactMessage } from "../../cards/CompactMessage"; -export async function RecentMessages({ - query, - users, -}: { - query: Filter; - users?: boolean | User[]; -}) { - const recentMessages = (await fetchMessages(query)).reverse(); +type Props = { users?: boolean | User[] } & ( + | { + query: Filter; + } + | { + userId: string; + } +); + +export async function RecentMessages(props: Props) { + const recentMessages = ( + (props as { query: {} }).query + ? await fetchMessages((props as { query: Filter }).query) + : await fetchMessagesByUser((props as { userId: string }).userId).catch( + () => [] + ) + ).reverse(); const userList = ( - users === true + props.users === true ? await fetchUsersById([...new Set(recentMessages.map((x) => x.author))]) - : Array.isArray(users) - ? users + : Array.isArray(props.users) + ? props.users : [] ).reduce((prev, next) => { prev[next._id] = next; @@ -38,6 +47,16 @@ export async function RecentMessages({ {/* enter reason for fetching */} + {!recentMessages.length && ( + <> +

+ No messages found +

+

+ You may be lacking permissions. +

+ + )} {recentMessages.map((message) => ( ( {strike.reason} - - {dayjs(decodeTime(strike._id))?.fromNow()} - + {dayjs(decodeTime(strike._id)).fromNow()} ))} diff --git a/lib/accessPermissions.ts b/lib/accessPermissions.ts index c783e07..dc4c9ed 100644 --- a/lib/accessPermissions.ts +++ b/lib/accessPermissions.ts @@ -13,14 +13,14 @@ type Permission = | `/fetch${"" | "/by-id" | "/by-user"}` | `/update${"" | "/discoverability"}`}` | `channels${"" | `/fetch${"" | "/by-id" | "/dm"}` | `/create${"" | "/dm"}`}` - | `messages${"" | `/fetch${"" | "/by-id"}`}` + | `messages${"" | `/fetch${"" | "/by-id" | "/by-user"}`}` | `reports${ | "" | `/fetch${ | "" | "/by-id" | "/open" - | `/related${"" | "/by-content" | "/by-user"}` + | `/related${"" | "/by-content" | "/by-user" | "/against-user"}` | `/snapshots${"" | "/by-report" | "/by-user"}`}` | `/update${ | "" @@ -36,24 +36,55 @@ type Permission = | `/update${"" | "/flags" | "/discoverability"}`}` | `users${ | "" - | `/fetch${"" | "/by-id" | "/memberships"}` + | `/fetch${ + | "" + | "/by-id" + | "/memberships" + | "/strikes" + | "/notices" + | "/relations"}` | `/create${"" | "/alert" | "/strike"}` | `/update${"" | "/badges"}` | `/action${"" | "/unsuspend" | "/suspend" | "/wipe" | "/ban"}`}`; const PermissionSets = { + // View open reports "view-open-reports": [ - // Required for viewing open reports "users/fetch/by-id", "reports/fetch/open", "reports/fetch/by-id", "reports/fetch/related", "reports/fetch/snapshots/by-report", ] as Permission[], + + // Edit reports + "edit-reports": [ + "reports/update/notes", + "reports/update/resolve", + "reports/update/reject", + "reports/update/reopen", + ] as Permission[], + + // Moderate users + "moderate-users": [ + "users/fetch/by-id", + "users/fetch/strikes", + "users/fetch/notices", + // "bots/fetch/by-user", + // "messages/fetch/by-user", + // "users/fetch/memberships", + // "servers/fetch", + "reports/fetch/related/by-user", + "reports/fetch/related/against-user", + ] as Permission[], }; const Roles = { - moderator: [...PermissionSets["view-open-reports"]], + moderator: [ + ...PermissionSets["view-open-reports"], + ...PermissionSets["edit-reports"], + ...PermissionSets["moderate-users"], + ], }; const ACL: Record> = { diff --git a/lib/db.ts b/lib/db.ts index 6f201ec..c062186 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -14,7 +14,8 @@ import type { } from "revolt-api"; import { ulid } from "ulid"; import { publishMessage } from "./redis"; -import { checkPermission } from "./accessPermissions"; +import { checkPermission, hasPermissionFromSession } from "./accessPermissions"; +import { PLATFORM_MOD_ID } from "./constants"; let client: MongoClient; @@ -121,7 +122,16 @@ export async function fetchUserById(id: string) { return await mongo() .db("revolt") .collection("users") - .findOne({ _id: id }); + .findOne( + { _id: id }, + { + projection: { + relations: (await hasPermissionFromSession("users/fetch/relations")) + ? 1 + : 0, + }, + } + ); } export async function fetchUsersById(ids: string[]) { @@ -130,7 +140,16 @@ export async function fetchUsersById(ids: string[]) { return await mongo() .db("revolt") .collection("users") - .find({ _id: { $in: ids } }) + .find( + { _id: { $in: ids } }, + { + projection: { + relations: (await hasPermissionFromSession("users/fetch/relations")) + ? 1 + : 0, + }, + } + ) .toArray(); } @@ -227,6 +246,26 @@ export async function fetchMessageById(id: string) { .findOne({ _id: id }); } +export async function fetchMessagesByUser(userId: string) { + await checkPermission("messages/fetch/by-user"); + + return await mongo() + .db("revolt") + .collection("messages") + .find({ author: userId }, { sort: { _id: -1 }, limit: 50 }) + .toArray(); +} + +export async function fetchMessagesByChannel(channelId: string) { + await checkPermission("messages/fetch/by-user"); + + return await mongo() + .db("revolt") + .collection("messages") + .find({ channel: channelId }, { sort: { _id: -1 }, limit: 50 }) + .toArray(); +} + export async function fetchMessages(query: Filter, limit = 50) { await checkPermission("messages/fetch"); @@ -254,7 +293,7 @@ export async function fetchOpenReports() { .toArray(); } -export async function fetchRelatedReports(contentId: string) { +export async function fetchRelatedReportsByContent(contentId: string) { await checkPermission("reports/fetch/related/by-content"); return await mongo() @@ -271,6 +310,57 @@ export async function fetchRelatedReports(contentId: string) { .toArray(); } +export async function fetchReportsByUser(userId: string) { + await checkPermission("reports/fetch/related/by-user"); + + return await mongo() + .db("revolt") + .collection("safety_reports") + .find( + { status: "Created", authorId: userId }, + { + sort: { + _id: -1, + }, + } + ) + .toArray(); +} + +export async function fetchReportsAgainstUser(userId: string) { + await checkPermission("reports/fetch/related/against-user"); + + const reportIdsInvolvingUser = await mongo() + .db("revolt") + .collection<{ _id: string; report_id: string; content: SnapshotContent }>( + "safety_snapshots" + ) + .find({ + $or: [{ "content._id": userId }, { "content.author": userId }], + }) + .toArray() + .then((snapshots) => [ + ...new Set(snapshots.map((snapshot) => snapshot.report_id)), + ]); + + return await mongo() + .db("revolt") + .collection("safety_reports") + .find( + { + _id: { + $in: reportIdsInvolvingUser, + }, + }, + { + sort: { + _id: -1, + }, + } + ) + .toArray(); +} + export async function fetchReports( query: Filter = { status: "Created" } ) { @@ -332,7 +422,7 @@ export async function fetchSnapshotsByReport(reportId: string) { } export async function fetchStrikesByUser(userId: string) { - await checkPermission("reports/fetch/snapshots/by-user"); + await checkPermission("users/fetch/strikes"); return await mongo() .db("revolt") @@ -348,6 +438,28 @@ export async function fetchStrikesByUser(userId: string) { .toArray(); } +export async function fetchNoticesByUser(userId: string) { + await checkPermission("users/fetch/notices"); + + const dm = await mongo() + .db("revolt") + .collection("channels") + .findOne({ + channel_type: "DirectMessage", + recipients: { + $all: [userId, PLATFORM_MOD_ID], + }, + }); + + if (!dm) return []; + + return await mongo() + .db("revolt") + .collection("messages") + .find({ channel: dm!._id }, { sort: { _id: -1 } }) + .toArray(); +} + export async function fetchBotsByUser(userId: string) { await checkPermission("bots/fetch/by-user");