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 { UserCard } from "@/components/cards/UserCard";
|
||||||
|
import { EmailClassificationCard } from "@/components/cards/authifier/EmailClassificationCard";
|
||||||
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
|
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 { notFound } from "next/navigation";
|
||||||
|
import { User } from "revolt-api";
|
||||||
|
|
||||||
export default async function User({
|
export default async function User({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { id: string; type: string };
|
params: { id: string; type: string };
|
||||||
}) {
|
}) {
|
||||||
|
const account = await fetchAccountById(params.id);
|
||||||
|
if (!account) return notFound();
|
||||||
|
|
||||||
const user = await fetchUserById(params.id);
|
const user = await fetchUserById(params.id);
|
||||||
// if (!user) return notFound();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<NavigationToolbar>Inspecting Account</NavigationToolbar>
|
<NavigationToolbar>Inspecting Account</NavigationToolbar>
|
||||||
{user && <UserCard user={user} subtitle="Associated User" />}
|
{user && <UserCard user={user} subtitle={account.email} />}
|
||||||
update password, reset 2FA, disable / undisable (disabled if pending
|
<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
|
delete), delete / cancel delete
|
||||||
<br />
|
<br />
|
||||||
list active 2FA methods without keys
|
list active 2FA methods without keys
|
||||||
<br />
|
<br />
|
||||||
email account
|
email account*/}
|
||||||
</div>
|
</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 { writeFile } from "fs/promises";
|
||||||
import { PLATFORM_MOD_ID } from "./constants";
|
import { PLATFORM_MOD_ID } from "./constants";
|
||||||
import mongo, {
|
import mongo, {
|
||||||
|
Account,
|
||||||
createDM,
|
createDM,
|
||||||
fetchChannels,
|
fetchChannels,
|
||||||
fetchMembershipsByUser,
|
fetchMembershipsByUser,
|
||||||
|
@ -179,6 +180,24 @@ export async function banUser(userId: string) {
|
||||||
return await wipeUser(userId, 4);
|
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) {
|
export async function updateServerFlags(serverId: string, flags: number) {
|
||||||
await mongo().db("revolt").collection<Server>("servers").updateOne(
|
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 });
|
.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) {
|
export async function fetchUserById(id: string) {
|
||||||
return await mongo()
|
return await mongo()
|
||||||
.db("revolt")
|
.db("revolt")
|
||||||
|
@ -210,3 +266,15 @@ export async function fetchBotsByUser(userId: string) {
|
||||||
.find({ owner: userId })
|
.find({ owner: userId })
|
||||||
.toArray();
|
.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