diff --git a/components/pages/inspector/AccountActions.tsx b/components/pages/inspector/AccountActions.tsx
index 7d599d9..e893fbd 100644
--- a/components/pages/inspector/AccountActions.tsx
+++ b/components/pages/inspector/AccountActions.tsx
@@ -8,7 +8,9 @@ import { User } from "revolt-api";
import {
cancelAccountDeletion,
changeAccountEmail,
+ deleteMFARecoveryCodes,
disableAccount,
+ disableMFA,
queueAccountDeletion,
restoreAccount,
verifyAccountEmail,
@@ -137,6 +139,77 @@ export function AccountActions({
+
+
+
+
+
+
+
+
+ Manage MFA
+
+
+ MFA is currently {
+ accountDraft.mfa?.totp_token?.status == "Pending"
+ ? "pending setup"
+ : (accountDraft.mfa?.totp_token?.status.toLowerCase() || "disabled")
+ }.
+
+ The account has {accountDraft.mfa?.recovery_codes ?? "no"} recovery codes.
+
+
+
+ {
+ try {
+ await deleteMFARecoveryCodes(account._id);
+ toast({
+ title: "MFA recovery codes cleared",
+ });
+ accountDraft.mfa!.recovery_codes = undefined;
+ } catch(e) {
+ toast({
+ title: "Failed to clear recovery codes",
+ description: String(e),
+ variant: "destructive",
+ })
+ }
+ }}
+ >
+ Clear recovery codes
+
+ {
+ try {
+ await disableMFA(account._id);
+ toast({
+ title: "MFA disabled",
+ });
+ accountDraft.mfa!.totp_token = undefined;
+ } catch(e) {
+ toast({
+ title: " Failed to disable MFA",
+ description: String(e),
+ variant: "destructive",
+ })
+ }
+ }}
+ >
+ Disable MFA
+
+ Close
+
+
+
diff --git a/lib/accessPermissions.ts b/lib/accessPermissions.ts
index a150873..b0909d1 100644
--- a/lib/accessPermissions.ts
+++ b/lib/accessPermissions.ts
@@ -8,7 +8,7 @@ type Permission =
| `accounts${
| ""
| `/fetch${"" | "/by-id" | "/by-email"}`
- | "/update/email"
+ | `/update${"" | "/email" | "/mfa"}`
| "/disable"
| "/restore"
| `/deletion${"" | "/queue" | "/cancel"}`}`
@@ -107,6 +107,7 @@ const PermissionSets = {
"accounts/deletion/queue",
"accounts/deletion/cancel",
"accounts/update/email",
+ "accounts/update/mfa",
] as Permission[],
// Moderate users
diff --git a/lib/actions.ts b/lib/actions.ts
index 00ebbc5..f55318e 100644
--- a/lib/actions.ts
+++ b/lib/actions.ts
@@ -195,6 +195,40 @@ export async function disableAccount(userId: string) {
});
}
+export async function deleteMFARecoveryCodes(userId: string) {
+ if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
+ await checkPermission("accounts/update/mfa", userId);
+
+ await mongo()
+ .db("revolt")
+ .collection("accounts")
+ .updateOne(
+ { _id: userId },
+ {
+ $unset: {
+ "mfa.recovery_codes": 1,
+ },
+ },
+ )
+}
+
+export async function disableMFA(userId: string) {
+ if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
+ await checkPermission("accounts/update/mfa", userId);
+
+ await mongo()
+ .db("revolt")
+ .collection("accounts")
+ .updateOne(
+ { _id: userId },
+ {
+ $unset: {
+ "mfa.totp_token": 1,
+ },
+ },
+ )
+}
+
export async function changeAccountEmail(userId: string, email: string) {
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
await checkPermission("accounts/update/email", userId);
diff --git a/lib/db.ts b/lib/db.ts
index d687c06..14d810c 100644
--- a/lib/db.ts
+++ b/lib/db.ts
@@ -1,6 +1,6 @@
"use server";
-import { Filter, MongoClient } from "mongodb";
+import { Filter, MongoClient, WithId } from "mongodb";
import type {
AccountStrike,
Bot,
@@ -104,6 +104,12 @@ export type Account = {
attempts: number;
expiry: string;
};
+ mfa?: {
+ totp_token?: {
+ status: "Pending" | "Enabled";
+ };
+ recovery_codes?: number;
+ };
};
export async function fetchAccountById(id: string) {
@@ -112,15 +118,31 @@ export async function fetchAccountById(id: string) {
return await mongo()
.db("revolt")
.collection("accounts")
- .findOne(
- { _id: id },
- {
- projection: {
- password: 0,
- mfa: 0,
+ .aggregate(
+ [
+ {
+ $match: { _id: id },
},
- }
- );
+ {
+ $project: {
+ password: 0,
+ "mfa.totp_token.secret": 0,
+ }
+ },
+ {
+ $set: {
+ // Replace recovery code array with amount of codes
+ "mfa.recovery_codes": {
+ $cond: {
+ if: { $isArray: "$mfa.recovery_codes" },
+ then: { $size: "$mfa.recovery_codes", },
+ else: undefined,
+ }
+ }
+ }
+ }
+ ]
+ ).next() as WithId;
}
export async function fetchSessionsByAccount(accountId: string) {