forked from administration/panel
feat: show email classification, disable / delete accounts
parent
45405fabd7
commit
aca5010b41
|
@ -1,26 +1,33 @@
|
|||
import { UserCard } from "@/components/cards/UserCard";
|
||||
import { EmailClassificationCard } from "@/components/cards/authifier/EmailClassificationCard";
|
||||
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
||||
import { fetchUserById } from "@/lib/db";
|
||||
import { AccountActions } from "@/components/inspector/AccountActions";
|
||||
import { fetchAccountById, fetchUserById } from "@/lib/db";
|
||||
import { notFound } from "next/navigation";
|
||||
import { User } from "revolt-api";
|
||||
|
||||
export default async function User({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string; type: string };
|
||||
}) {
|
||||
const account = await fetchAccountById(params.id);
|
||||
if (!account) return notFound();
|
||||
|
||||
const user = await fetchUserById(params.id);
|
||||
// if (!user) return notFound();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<NavigationToolbar>Inspecting Account</NavigationToolbar>
|
||||
{user && <UserCard user={user} subtitle="Associated User" />}
|
||||
update password, reset 2FA, disable / undisable (disabled if pending
|
||||
{user && <UserCard user={user} subtitle={account.email} />}
|
||||
<AccountActions account={account} user={user as User} />
|
||||
<EmailClassificationCard email={account.email} />
|
||||
{/*TODO? update password, reset 2FA, disable / undisable (disabled if pending
|
||||
delete), delete / cancel delete
|
||||
<br />
|
||||
list active 2FA methods without keys
|
||||
<br />
|
||||
email account
|
||||
email account*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { fetchAuthifierEmailClassification } from "@/lib/db";
|
||||
|
||||
export async function EmailClassificationCard({ email }: { email: string }) {
|
||||
const provider = email.split("@").pop() ?? "";
|
||||
const providerInfo = await fetchAuthifierEmailClassification(provider);
|
||||
if (!providerInfo) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Email Classification</CardTitle>
|
||||
<CardDescription>{providerInfo.classification}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
"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,
|
||||
disableAccount,
|
||||
queueAccountDeletion,
|
||||
restoreAccount,
|
||||
} 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";
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export function AccountActions({
|
||||
account,
|
||||
user,
|
||||
}: {
|
||||
account: Account;
|
||||
user?: User;
|
||||
}) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const [accountDraft, setAccountDraft] = useState(account);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<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>
|
||||
);
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
import { writeFile } from "fs/promises";
|
||||
import { PLATFORM_MOD_ID } from "./constants";
|
||||
import mongo, {
|
||||
Account,
|
||||
createDM,
|
||||
fetchChannels,
|
||||
fetchMembershipsByUser,
|
||||
|
@ -179,6 +180,24 @@ export async function banUser(userId: string) {
|
|||
return await wipeUser(userId, 4);
|
||||
}
|
||||
|
||||
export async function unsuspendUser(userId: string) {
|
||||
await restoreAccount(userId);
|
||||
|
||||
await mongo()
|
||||
.db("revolt")
|
||||
.collection<User>("users")
|
||||
.updateOne(
|
||||
{
|
||||
_id: userId,
|
||||
},
|
||||
{
|
||||
$unset: {
|
||||
flags: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateServerFlags(serverId: string, flags: number) {
|
||||
await mongo().db("revolt").collection<Server>("servers").updateOne(
|
||||
{
|
||||
|
@ -237,3 +256,60 @@ export async function updateBotDiscoverability(botId: string, state: boolean) {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function restoreAccount(accountId: string) {
|
||||
await mongo()
|
||||
.db("revolt")
|
||||
.collection<Account>("accounts")
|
||||
.updateOne(
|
||||
{
|
||||
_id: accountId,
|
||||
},
|
||||
{
|
||||
$unset: {
|
||||
disabled: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function queueAccountDeletion(accountId: string) {
|
||||
const twoWeeksFuture = new Date();
|
||||
twoWeeksFuture.setDate(twoWeeksFuture.getDate() + 14);
|
||||
|
||||
const $set: Partial<Account> = {
|
||||
disabled: true,
|
||||
deletion: {
|
||||
status: "Scheduled",
|
||||
after: twoWeeksFuture.toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
await disableAccount(accountId);
|
||||
await mongo().db("revolt").collection<Account>("accounts").updateOne(
|
||||
{
|
||||
_id: accountId,
|
||||
},
|
||||
{
|
||||
$set,
|
||||
}
|
||||
);
|
||||
|
||||
return $set;
|
||||
}
|
||||
|
||||
export async function cancelAccountDeletion(accountId: string) {
|
||||
await mongo()
|
||||
.db("revolt")
|
||||
.collection<Account>("accounts")
|
||||
.updateOne(
|
||||
{
|
||||
_id: accountId,
|
||||
},
|
||||
{
|
||||
$unset: {
|
||||
deletion: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
68
lib/db.ts
68
lib/db.ts
|
@ -33,6 +33,62 @@ export async function fetchBotById(id: string) {
|
|||
.findOne({ _id: id });
|
||||
}
|
||||
|
||||
export type Account = {
|
||||
_id: string;
|
||||
email: string;
|
||||
email_normalised: string;
|
||||
verification:
|
||||
| {
|
||||
status: "Verified";
|
||||
}
|
||||
| {
|
||||
status: "Pending";
|
||||
token: string;
|
||||
expiry: string;
|
||||
}
|
||||
| {
|
||||
status: "Moving";
|
||||
new_email: string;
|
||||
token: string;
|
||||
expiry: string;
|
||||
};
|
||||
password_reset: null | {
|
||||
token: string;
|
||||
expiry: string;
|
||||
};
|
||||
deletion:
|
||||
| null
|
||||
| {
|
||||
status: "WaitingForVerification";
|
||||
token: string;
|
||||
expiry: string;
|
||||
}
|
||||
| {
|
||||
status: "Scheduled";
|
||||
after: string;
|
||||
};
|
||||
disabled: boolean;
|
||||
lockout: null | {
|
||||
attempts: number;
|
||||
expiry: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function fetchAccountById(id: string) {
|
||||
return await mongo()
|
||||
.db("revolt")
|
||||
.collection<Account>("accounts")
|
||||
.findOne(
|
||||
{ _id: id },
|
||||
{
|
||||
projection: {
|
||||
password: 0,
|
||||
mfa: 0,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchUserById(id: string) {
|
||||
return await mongo()
|
||||
.db("revolt")
|
||||
|
@ -210,3 +266,15 @@ export async function fetchBotsByUser(userId: string) {
|
|||
.find({ owner: userId })
|
||||
.toArray();
|
||||
}
|
||||
|
||||
export type EmailClassification = {
|
||||
_id: string;
|
||||
classification: string;
|
||||
};
|
||||
|
||||
export async function fetchAuthifierEmailClassification(provider: string) {
|
||||
return await mongo()
|
||||
.db("authifier")
|
||||
.collection<EmailClassification>("email_classification")
|
||||
.findOne({ _id: provider });
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue