diff --git a/components/pages/inspector/UserActions.tsx b/components/pages/inspector/UserActions.tsx index 2936bc7..42bd5ae 100644 --- a/components/pages/inspector/UserActions.tsx +++ b/components/pages/inspector/UserActions.tsx @@ -23,8 +23,10 @@ import { Input } from "../../ui/input"; import { banUser, closeReportsByUser, + resetBotToken, sendAlert, suspendUser, + transferBot, unsuspendUser, updateBotDiscoverability, updateUserBadges, @@ -37,6 +39,7 @@ import { Card, CardHeader } from "../../ui/card"; import { cn } from "@/lib/utils"; import { decodeTime } from "ulid"; import { Checkbox } from "@/components/ui/checkbox"; +import UserSelector from "@/components/ui/user-selector"; const badges = [1, 2, 4, 8, 16, 32, 128, 0, 256, 512, 1024]; @@ -53,6 +56,8 @@ export function UserActions({ user, bot }: { user: User; bot?: Bot }) { displayName: false, status: false, }); + const [transferTarget, setTransferTarget] = useState(null); + const [transferResetToken, setTransferResetToken] = useState(true); const userInaccessible = userDraft.flags === 4 || userDraft.flags === 2; @@ -416,6 +421,86 @@ export function UserActions({ user, bot }: { user: User; bot?: Bot }) { + + + + + + + Reset token + + + Re-roll this bot's authentication token. This will not disconnect active connections. + + + + + Cancel + resetBotToken(user._id) + .then(() => toast({ + title: "Reset bot token", + })) + .catch((e) => toast({ + title: "Failed to reset token", + description: String(e), + variant: "destructive", + })) + } + > + Reset + + + + + + + + + + + + Transfer bot + + + Transfer this bot to a new owner. + + + setTransferResetToken(!!e)} + > + Also reset token + + + + + Cancel + transferBot(user._id, transferTarget!._id, transferResetToken) + .then(() => toast({ + title: "Reset bot token", + })) + .catch((e) => toast({ + title: "Failed to reset token", + description: String(e), + variant: "destructive", + })) + .finally(() => { + setTransferResetToken(true); + setTransferTarget(null); + }) + } + > + Transfer + + + + + diff --git a/lib/accessPermissions.ts b/lib/accessPermissions.ts index f9e18e7..59f4196 100644 --- a/lib/accessPermissions.ts +++ b/lib/accessPermissions.ts @@ -24,7 +24,7 @@ type Permission = | `bots${ | "" | `/fetch${"" | "/by-id" | "/by-user"}` - | `/update${"" | "/discoverability"}`}` + | `/update${"" | "/discoverability" | "/owner" | "/reset-token"}`}` | `channels${ | "" | `/fetch${"" | "/by-id" | "/by-server" | "/dm" | "/invites"}` @@ -139,7 +139,10 @@ const PermissionSets = { "users/update/badges", "servers/update/owner", - "servers/update/add-member", + + "bots/fetch/by-user", + "bots/update/reset-token", + "bots/update/owner", "accounts/fetch/by-id", "accounts/fetch/by-email", @@ -164,6 +167,9 @@ const PermissionSets = { "users/fetch/notices", "bots/fetch/by-user", + "bots/update/reset-token", + "bots/update/owner", + // "messages/fetch/by-user", // "users/fetch/memberships", "servers/fetch", @@ -175,6 +181,8 @@ const PermissionSets = { "channels/create/dm", "servers/update/quarantine", + "servers/update/owner", + "servers/update/add-member", "backup/fetch", "reports/fetch/related/by-user", diff --git a/lib/actions.ts b/lib/actions.ts index e138e0a..210bf27 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -28,6 +28,7 @@ import { } from "revolt-api"; import { checkPermission } from "./accessPermissions"; import { Long } from "mongodb"; +import { nanoid } from "nanoid"; export async function sendAlert(userId: string, content: string) { await checkPermission("users/create/alert", userId, { content }); @@ -819,6 +820,84 @@ export async function updateBotDiscoverability(botId: string, state: boolean) { ); } +export async function resetBotToken(botId: string) { + await checkPermission("bots/update/reset-token", { botId }); + + // Should generate tokens the exact same as the backend generates them: + // https://github.com/revoltchat/backend/blob/41f20c2239ed6307ad821b321d13240dc6ff3327/crates/core/database/src/models/bots/model.rs#L106 + + await mongo() + .db("revolt") + .collection("bots") + .updateOne( + { + _id: botId, + }, + { + $set: { + token: nanoid(64), + }, + }, + ); +} + +export async function transferBot(botId: string, ownerId: string, resetToken: boolean) { + await checkPermission("bots/update/owner", { botId, ownerId, resetToken }); + + if (resetToken) { + await checkPermission("bots/update/reset-token", { botId }); + } + + await mongo() + .db("revolt") + .collection("bots") + .updateOne( + { + _id: botId, + }, + { + $set: { + owner: ownerId, + ...( + resetToken + ? { + token: nanoid(64), + } + : {} + ), + }, + }, + ); + + await mongo() + .db("revolt") + .collection("users") + .updateOne( + { + _id: botId, + }, + { + $set: { + "bot.owner": ownerId, + }, + }, + ); + + // This doesn't appear to work, maybe Revite can't handle it. I'll leave it in regardless. + await publishMessage( + botId, + { + type: "UserUpdate", + id: botId, + data: { + bot: { + owner: ownerId, + }, + }, + }, + ); +} + export async function restoreAccount(accountId: string) { if (RESTRICT_ACCESS_LIST.includes(accountId)) throw "restricted access"; await checkPermission("accounts/restore", accountId); diff --git a/package.json b/package.json index a3c750c..fa0e253 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "lodash.debounce": "^4.0.8", "lucide-react": "^0.263.0", "mongodb": "^5.7.0", + "nanoid": "^5.0.1", "next": "13.4.12", "next-auth": "^4.22.3", "ntfy": "^1.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d783a2..d1f5f29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ dependencies: mongodb: specifier: ^5.7.0 version: 5.7.0 + nanoid: + specifier: ^5.0.1 + version: 5.0.1 next: specifier: 13.4.12 version: 13.4.12(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1) @@ -3754,6 +3757,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@5.0.1: + resolution: {integrity: sha512-vWeVtV5Cw68aML/QaZvqN/3QQXc6fBfIieAlu05m7FZW2Dgb+3f0xc0TTxuJW+7u30t7iSDTV/j3kVI0oJqIfQ==} + engines: {node: ^18 || >=20} + hasBin: true + dev: false + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: false