feat: suspend users (and send emails)

This commit is contained in:
Paul Makles
2024-08-31 17:12:39 +01:00
parent d4baff4947
commit a472a56c9b
13 changed files with 154 additions and 38 deletions
+3
View File
@@ -39,6 +39,9 @@ next-env.d.ts
client.crt
client.key
# backend related files
Revolt.toml
# local files
exports/*
!exports/.gitkeep
@@ -12,10 +12,11 @@ export function ManageAccount({
attempts: number;
}) {
return (
// TODO
<Flex direction="row" gap="2">
<Button>Disable Account</Button>
<Button>Queue Deletion</Button>
<Button disabled={attempts === 0}>
<Button disabled>Disable Account</Button>
<Button disabled>Queue Deletion</Button>
<Button disabled={true || attempts === 0}>
Reset Lockout {attempts > 0 && <>({attempts} failed attempts)</>}
</Button>
</Flex>
+12 -3
View File
@@ -3,6 +3,7 @@
import { getScopedUser } from "@/lib/auth";
import { RBAC_PERMISSION_MODERATION_AGENT } from "@/lib/auth/rbacInternal";
import { createChangelog } from "@/lib/core";
import { suspendUser } from "@/lib/database/revolt";
import { createOrFindDM } from "@/lib/database/revolt/channels";
import { sendMessage } from "@/lib/database/revolt/messages";
import { findCaseById } from "@/lib/database/revolt/safety_cases";
@@ -16,6 +17,7 @@ export async function strikeUser(
caseId: string | undefined,
duration: "7" | "14" | "indefinite",
) {
const userEmail = await getScopedUser(RBAC_PERMISSION_MODERATION_AGENT);
if (caseId && !(await findCaseById(caseId))) throw "Case doesn't exist?";
const strike = await createStrike(
@@ -29,8 +31,7 @@ export async function strikeUser(
caseId,
);
const userEmail = await getScopedUser(RBAC_PERMISSION_MODERATION_AGENT);
await createChangelog(userEmail, {
const changelog = await createChangelog(userEmail, {
object: {
type: "User",
id: userId,
@@ -55,6 +56,14 @@ export async function strikeUser(
}),
});
if (type === "suspension") {
await suspendUser(
userId,
duration === "indefinite" ? 0 : parseInt(duration),
reason,
);
}
if (type !== "ban") {
const channel = await createOrFindDM(
userId,
@@ -78,5 +87,5 @@ export async function strikeUser(
});
}
return strike;
return { changelog, strike };
}
@@ -1,5 +1,6 @@
"use client";
import { consumeChangelog } from "@/components/core/admin/changelogs/helpers";
import { Strike } from "@/lib/database/revolt/safety_strikes";
import { useMutation } from "@tanstack/react-query";
import dayjs from "dayjs";
@@ -76,7 +77,16 @@ export function UserStrikeActions({
if (type === "ban" && !confirm)
return alert("Not banning, check the confirmation checkbox!");
const strike = await strikeUser(id, type, reason, context, caseId, days);
const { changelog, strike } = await strikeUser(
id,
type,
reason,
context,
caseId,
days,
);
consumeChangelog(changelog);
addStrike?.(strike);
if (type === "suspension") {
@@ -224,7 +234,10 @@ export function UserStrikeActions({
<AlertDialog.Trigger>
<Button
color="red"
disabled={mutation.isPending || actualFlags === USER_FLAG_BANNED}
// TODO
disabled={
true || mutation.isPending || actualFlags === USER_FLAG_BANNED
}
>
{actualFlags === USER_FLAG_BANNED ? "Banned" : "Ban"}
</Button>
BIN
View File
Binary file not shown.
+35 -1
View File
@@ -20,6 +20,7 @@ import {
PaperPlaneIcon,
PlusIcon,
TextIcon,
ValueNoneIcon,
} from "@radix-ui/react-icons";
import { Avatar, Badge, Card, Flex, Text } from "@radix-ui/themes";
@@ -218,6 +219,39 @@ const ChangeRenderer: Renderers = {
</>
),
}),
"user/strike": (change) => ({
type: "short",
icon: <ValueNoneIcon />,
color: "red",
description: (
<>
<Text color="plum">{change.userEmail}</Text> created a strike for{" "}
<Text color="blue">{change.reason.join(", ")}</Text>
</>
),
}),
"user/suspend": (change) => ({
type: "short",
icon: <ValueNoneIcon />,
color: "red",
description: (
<>
<Text color="plum">{change.userEmail}</Text> suspended this user for{" "}
<Text color="blue">{change.reason.join(", ")}</Text>
</>
),
}),
"user/ban": (change) => ({
type: "short",
icon: <ValueNoneIcon />,
color: "red",
description: (
<>
<Text color="plum">{change.userEmail}</Text> banned this user for{" "}
<Text color="blue">{change.reason.join(", ")}</Text>
</>
),
}),
"user/export": (change) => ({
type: "short",
icon: <ArchiveIcon />,
@@ -225,7 +259,7 @@ const ChangeRenderer: Renderers = {
description: (
<>
<Text color="plum">{change.userEmail}</Text> created an export of{" "}
{change.exportType} type
<Text color="blue">{change.exportType}</Text> type
</>
),
}),
+2 -1
View File
@@ -3,13 +3,14 @@ export type Hr = {
_id: string;
name: string;
email: string;
status: "Active" | "Pending" | "Inactive";
status: "Active" | "Pending" | "Inactive" | "Retired";
positions: string[];
roles: string[];
approvalRequest?: {
reason: string;
requestee: string;
};
notes?: string;
};
Position: {
_id: string;
+3
View File
@@ -18,6 +18,9 @@ export function fetchPeople() {
projection: {
approvalRequest: 0,
},
sort: {
name: 1,
},
},
)
.toArray();
+37 -1
View File
@@ -1,10 +1,16 @@
import { Document, MongoClient } from "mongodb";
import { Database, Err, database, init } from "revolt-nodejs-bindings";
/**
* Global handle shared in process
* Global handle to Mongo shared in process
*/
let client: MongoClient;
/**
* Global handle to binding database shared in process
*/
let bindDatabase: Database;
/**
* Fetch handle to MongoDB client
* @returns Mongo client
@@ -17,6 +23,36 @@ function mongoClient() {
return client;
}
/**
* Fetch handle to binding database
* @returns database
*/
export function revoltDb() {
if (!bindDatabase) {
bindDatabase = database();
}
return bindDatabase;
}
/**
* Call a procedure from the Revolt backend
*/
export async function callProcedure<A extends any[], R>(
fn: (...args: A) => R,
...args: A
): Promise<R> {
await init();
const result = await fn.bind(revoltDb())(...args);
if ((result as { error?: Err }).error)
throw new Error(
(result as { error: Err }).error.type +
" in " +
(result as { error: Err }).error.location,
);
return result;
}
/**
* Create a collection handle generator for given parameters
* @param db Database Name
+3 -26
View File
@@ -1,8 +1,7 @@
import { publishPrivate } from "@/lib/events";
import Revolt from "revolt-nodejs-bindings";
import { API } from "revolt.js";
import { ulid } from "ulid";
import { createCollectionFn } from "..";
import { callProcedure, createCollectionFn } from "..";
export type RevoltChannel = API.Channel;
@@ -15,27 +14,5 @@ const channelCol = createCollectionFn<RevoltChannel>("revolt", "channels");
* @returns DM Channel
*/
export async function createOrFindDM(userA: string, userB: string) {
let dm = await channelCol().findOne({
channel_type: "DirectMessage",
recipients: { $all: [userA, userB] },
});
if (!dm) {
dm = {
_id: ulid(),
channel_type: "DirectMessage",
active: true,
recipients: [userA, userB],
};
await channelCol().insertOne(dm);
for (const user of [userA, userB])
await publishPrivate(user, {
type: "ChannelCreate",
...dm,
});
}
return dm;
return callProcedure(Revolt.proc_channels_create_dm, userA, userB);
}
+23 -1
View File
@@ -1,6 +1,7 @@
import Revolt from "revolt-nodejs-bindings";
import { API } from "revolt.js";
import { createCollectionFn } from "..";
import { callProcedure, createCollectionFn } from "..";
export type RevoltUser = API.User;
export type RevoltUserInfo = Omit<RevoltUser, "relations"> & {
@@ -42,3 +43,24 @@ const userCol = createCollectionFn<RevoltUser>("revolt", "users");
export function fetchUserById(id: string) {
return userCol().findOne({ _id: id });
}
/**
* Suspend user by given ID
* @param userId User ID
* @param duration Duration (in days), set to 0 for indefinite
* @param reasons Reasons
*/
export async function suspendUser(
userId: string,
duration: number,
reasons: string[],
) {
let user = await callProcedure(Revolt.database_fetch_user, userId);
await callProcedure(
Revolt.proc_users_suspend,
user,
duration,
reasons.join("|"),
);
}
+15
View File
@@ -3,6 +3,21 @@ const nextConfig = {
experimental: {
reactCompiler: true,
},
webpack: (config, { dev, isServer, webpack, nextRuntime }) => {
config.module.rules.push({
test: /\.node$/,
use: [
{
loader: "nextjs-node-loader",
options: {
// flags: os.constants.dlopen.RTLD_NOW,
outputPath: config.output.path,
},
},
],
});
return config;
},
};
module.exports = nextConfig;
+2
View File
@@ -25,12 +25,14 @@
"mongodb": "^6.3.0",
"next": "rc",
"next-auth": "^4.24.5",
"nextjs-node-loader": "^1.1.5",
"node-cron": "^3.0.3",
"react": "rc",
"react-dom": "rc",
"react-markdown": "^9.0.1",
"redis": "^4.6.13",
"revolt-api": "^0.6.9",
"revolt-nodejs-bindings": "0.7.15-rev0.0.2",
"revolt.js": "^7.0.0-beta.11",
"scikitjs": "^1.24.0",
"string-comparison": "^1.3.0",