1
0
Fork 0

feat: MFA management

user-stream
Lea 2023-08-10 16:57:00 +02:00 committed by insert
parent 7ba9565df7
commit e575389a66
4 changed files with 140 additions and 10 deletions

View File

@ -8,7 +8,9 @@ import { User } from "revolt-api";
import { import {
cancelAccountDeletion, cancelAccountDeletion,
changeAccountEmail, changeAccountEmail,
deleteMFARecoveryCodes,
disableAccount, disableAccount,
disableMFA,
queueAccountDeletion, queueAccountDeletion,
restoreAccount, restoreAccount,
verifyAccountEmail, verifyAccountEmail,
@ -138,6 +140,77 @@ export function AccountActions({
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </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> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button <Button

View File

@ -8,7 +8,7 @@ type Permission =
| `accounts${ | `accounts${
| "" | ""
| `/fetch${"" | "/by-id" | "/by-email"}` | `/fetch${"" | "/by-id" | "/by-email"}`
| "/update/email" | `/update${"" | "/email" | "/mfa"}`
| "/disable" | "/disable"
| "/restore" | "/restore"
| `/deletion${"" | "/queue" | "/cancel"}`}` | `/deletion${"" | "/queue" | "/cancel"}`}`
@ -107,6 +107,7 @@ const PermissionSets = {
"accounts/deletion/queue", "accounts/deletion/queue",
"accounts/deletion/cancel", "accounts/deletion/cancel",
"accounts/update/email", "accounts/update/email",
"accounts/update/mfa",
] as Permission[], ] as Permission[],
// Moderate users // Moderate users

View File

@ -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) { export async function changeAccountEmail(userId: string, email: string) {
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access"; if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
await checkPermission("accounts/update/email", userId); await checkPermission("accounts/update/email", userId);

View File

@ -1,6 +1,6 @@
"use server"; "use server";
import { Filter, MongoClient } from "mongodb"; import { Filter, MongoClient, WithId } from "mongodb";
import type { import type {
AccountStrike, AccountStrike,
Bot, Bot,
@ -104,6 +104,12 @@ export type Account = {
attempts: number; attempts: number;
expiry: string; expiry: string;
}; };
mfa?: {
totp_token?: {
status: "Pending" | "Enabled";
};
recovery_codes?: number;
};
}; };
export async function fetchAccountById(id: string) { export async function fetchAccountById(id: string) {
@ -112,15 +118,31 @@ export async function fetchAccountById(id: string) {
return await mongo() return await mongo()
.db("revolt") .db("revolt")
.collection<Account>("accounts") .collection<Account>("accounts")
.findOne( .aggregate(
{ _id: id }, [
{ {
projection: { $match: { _id: id },
password: 0,
mfa: 0,
}, },
{
$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) { export async function fetchSessionsByAccount(accountId: string) {