1
0
Fork 0

feat: show email classification, disable / delete accounts

fix-1
Paul Makles 2023-07-28 12:10:37 +01:00
parent 45405fabd7
commit aca5010b41
No known key found for this signature in database
GPG Key ID: 5059F398521BB0F6
5 changed files with 342 additions and 5 deletions

View File

@ -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>
); );
} }

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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,
},
}
);
}

View File

@ -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 });
}