forked from administration/panel
337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Button } from "../../ui/button";
|
|
import { useToast } from "../../ui/use-toast";
|
|
import type { Account } from "@/lib/db";
|
|
import { User } from "revolt-api";
|
|
import {
|
|
cancelAccountDeletion,
|
|
changeAccountEmail,
|
|
deleteMFARecoveryCodes,
|
|
disableAccount,
|
|
disableMFA,
|
|
queueAccountDeletion,
|
|
restoreAccount,
|
|
verifyAccountEmail,
|
|
} from "@/lib/actions";
|
|
import dayjs from "dayjs";
|
|
|
|
import relativeTime from "dayjs/plugin/relativeTime";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "../../ui/alert-dialog";
|
|
import { AlertDialogDescription } from "@radix-ui/react-alert-dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
dayjs.extend(relativeTime);
|
|
|
|
export function AccountActions({
|
|
account,
|
|
user,
|
|
}: {
|
|
account: Account;
|
|
user?: User;
|
|
}) {
|
|
const { toast } = useToast();
|
|
|
|
const [accountDraft, setAccountDraft] = useState(account);
|
|
const [emailDraft, setEmailDraft] = useState("");
|
|
|
|
return (
|
|
<div className="flex gap-2">
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button className="flex-1">
|
|
Change Email
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
Update account email
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
<Input
|
|
placeholder={account.email}
|
|
onChange={(e) => setEmailDraft(e.currentTarget.value)}
|
|
value={emailDraft}
|
|
/>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
disabled={!/^.+@.+$/.test(emailDraft)}
|
|
onClick={async () => {
|
|
try {
|
|
await changeAccountEmail(account._id, emailDraft);
|
|
setEmailDraft("");
|
|
toast({ title: "Updated email" });
|
|
window.location.reload();
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to execute action",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Change
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
className="flex-1"
|
|
disabled={accountDraft.verification.status == "Verified"}
|
|
>
|
|
{accountDraft.verification.status == "Verified" ? "Email is verified" : "Verify email"}
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
Mark Email as verified
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Verification status is currently {accountDraft.verification.status}.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={async () => {
|
|
try {
|
|
await verifyAccountEmail(account._id);
|
|
toast({ title: "Verified email" });
|
|
setAccountDraft({ ...accountDraft, verification: { status: "Verified" } });
|
|
} catch(e) {
|
|
toast({
|
|
title: "Failed to verify",
|
|
description: String(e),
|
|
variant: "destructive",
|
|
})
|
|
}
|
|
}}
|
|
>
|
|
Mark verified
|
|
</AlertDialogAction>
|
|
<AlertDialogAction
|
|
disabled={!(accountDraft.verification.status != "Verified" && accountDraft.verification.token)}
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(`https://app.revolt.chat/login/verify/${(accountDraft.verification as any).token}`);
|
|
toast({ title: "Copied verification link" })
|
|
}}
|
|
>
|
|
Copy link
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</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
|
|
className="flex-1"
|
|
disabled={accountDraft.deletion?.status === "Scheduled"}
|
|
>
|
|
{accountDraft.disabled ? "Restore Access" : "Disable"}
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
Are you sure you want to{" "}
|
|
{accountDraft.disabled ? "restore" : "disable"} this account?
|
|
</AlertDialogTitle>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={async () => {
|
|
try {
|
|
if (accountDraft.disabled) {
|
|
await restoreAccount(account._id);
|
|
setAccountDraft((account) => ({
|
|
...account!,
|
|
disabled: false,
|
|
}));
|
|
toast({
|
|
title: "Restored account",
|
|
});
|
|
} else {
|
|
await disableAccount(account._id);
|
|
setAccountDraft((account) => ({
|
|
...account!,
|
|
disabled: true,
|
|
}));
|
|
toast({
|
|
title: "Disabled account",
|
|
});
|
|
}
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to execute action",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
{accountDraft.disabled ? "Restore" : "Disable"}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
className="flex-1"
|
|
disabled={
|
|
user?.flags
|
|
? user?.flags === 2 ||
|
|
accountDraft.deletion?.status !== "Scheduled"
|
|
: false
|
|
}
|
|
>
|
|
{accountDraft.deletion?.status === "Scheduled"
|
|
? `Cancel Deletion (${dayjs(
|
|
(account.deletion as any)?.after
|
|
).fromNow()})`
|
|
: "Queue Deletion"}
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
Are you sure you want to{" "}
|
|
{accountDraft.deletion?.status === "Scheduled"
|
|
? "cancel deletion of"
|
|
: "queue deletion for"}{" "}
|
|
this account?
|
|
</AlertDialogTitle>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={async () => {
|
|
try {
|
|
if (accountDraft.deletion?.status === "Scheduled") {
|
|
await cancelAccountDeletion(account._id);
|
|
setAccountDraft((account) => ({
|
|
...account,
|
|
deletion: null,
|
|
}));
|
|
toast({
|
|
title: "Cancelled account deletion",
|
|
});
|
|
} else {
|
|
const $set = await queueAccountDeletion(account._id);
|
|
setAccountDraft((account) => ({ ...account!, ...$set }));
|
|
toast({
|
|
title: "Queued account for deletion",
|
|
});
|
|
}
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to execute action",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
{accountDraft.deletion?.status === "Scheduled"
|
|
? "Unqueue"
|
|
: "Queue"}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|