forked from administration/panel
feat: MFA management
parent
6394705f1e
commit
d19e15f36f
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
40
lib/db.ts
40
lib/db.ts
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue