mirror of
https://github.com/stoatchat/service-admin-panel.git
synced 2026-06-30 21:47:56 -04:00
feat: suspend users (and send emails)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
</>
|
||||
),
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,6 +18,9 @@ export function fetchPeople() {
|
||||
projection: {
|
||||
approvalRequest: 0,
|
||||
},
|
||||
sort: {
|
||||
name: 1,
|
||||
},
|
||||
},
|
||||
)
|
||||
.toArray();
|
||||
|
||||
+37
-1
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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("|"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user