forked from administration/panel
feat: MFA management
parent
6394705f1e
commit
d19e15f36f
|
@ -8,7 +8,9 @@ import { User } from "revolt-api";
|
|||
import {
|
||||
cancelAccountDeletion,
|
||||
changeAccountEmail,
|
||||
deleteMFARecoveryCodes,
|
||||
disableAccount,
|
||||
disableMFA,
|
||||
queueAccountDeletion,
|
||||
restoreAccount,
|
||||
verifyAccountEmail,
|
||||
|
@ -138,6 +140,77 @@ export function AccountActions({
|
|||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
className="flex-1"
|
||||
disabled={!accountDraft.mfa?.totp_token?.status}
|
||||
>
|
||||
MFA {accountDraft.mfa?.totp_token?.status.toLowerCase() || "disabled"}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Manage MFA
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
MFA is currently {
|
||||
accountDraft.mfa?.totp_token?.status == "Pending"
|
||||
? "pending setup"
|
||||
: (accountDraft.mfa?.totp_token?.status.toLowerCase() || "disabled")
|
||||
}.
|
||||
<br />
|
||||
The account has {accountDraft.mfa?.recovery_codes ?? "no"} recovery codes.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction
|
||||
className="hover:bg-red-800"
|
||||
disabled={accountDraft.mfa?.recovery_codes == null}
|
||||
onClick={async () => {
|
||||
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
|
||||
</AlertDialogAction>
|
||||
<AlertDialogAction
|
||||
className="hover:bg-red-800"
|
||||
onClick={async () => {
|
||||
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
|
||||
</AlertDialogAction>
|
||||
<AlertDialogCancel>Close</AlertDialogCancel>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Account>("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<Account>("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);
|
||||
|
|
40
lib/db.ts
40
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<Account>("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<Account>;
|
||||
}
|
||||
|
||||
export async function fetchSessionsByAccount(accountId: string) {
|
||||
|
|
Loading…
Reference in New Issue