From 7a1e6394831fb07e303c5ed0900dfe1ea4820de5 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:12:47 +0200 Subject: [chore] Refactor settings panel routing (and other fixes) (#2864) --- .../views/moderation/accounts/detail/actions.tsx | 89 +++++ .../moderation/accounts/detail/handlesignup.tsx | 118 ++++++ .../views/moderation/accounts/detail/index.tsx | 167 +++++++++ .../settings/views/moderation/accounts/index.tsx | 35 ++ .../views/moderation/accounts/pending/index.tsx | 40 +++ .../views/moderation/accounts/search/index.tsx | 131 +++++++ .../views/moderation/domain-permissions/detail.tsx | 262 ++++++++++++++ .../domain-permissions/export-format-table.tsx | 65 ++++ .../views/moderation/domain-permissions/form.tsx | 153 ++++++++ .../domain-permissions/import-export.tsx | 88 +++++ .../moderation/domain-permissions/overview.tsx | 197 ++++++++++ .../moderation/domain-permissions/process.tsx | 400 +++++++++++++++++++++ .../settings/views/moderation/reports/detail.tsx | 243 +++++++++++++ .../settings/views/moderation/reports/overview.tsx | 99 +++++ .../settings/views/moderation/reports/username.tsx | 54 +++ web/source/settings/views/moderation/routes.tsx | 201 +++++++++++ 16 files changed, 2342 insertions(+) create mode 100644 web/source/settings/views/moderation/accounts/detail/actions.tsx create mode 100644 web/source/settings/views/moderation/accounts/detail/handlesignup.tsx create mode 100644 web/source/settings/views/moderation/accounts/detail/index.tsx create mode 100644 web/source/settings/views/moderation/accounts/index.tsx create mode 100644 web/source/settings/views/moderation/accounts/pending/index.tsx create mode 100644 web/source/settings/views/moderation/accounts/search/index.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/detail.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/export-format-table.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/form.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/import-export.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/overview.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/process.tsx create mode 100644 web/source/settings/views/moderation/reports/detail.tsx create mode 100644 web/source/settings/views/moderation/reports/overview.tsx create mode 100644 web/source/settings/views/moderation/reports/username.tsx create mode 100644 web/source/settings/views/moderation/routes.tsx (limited to 'web/source/settings/views/moderation') diff --git a/web/source/settings/views/moderation/accounts/detail/actions.tsx b/web/source/settings/views/moderation/accounts/detail/actions.tsx new file mode 100644 index 000000000..74c5371f1 --- /dev/null +++ b/web/source/settings/views/moderation/accounts/detail/actions.tsx @@ -0,0 +1,89 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import React from "react"; + +import { useActionAccountMutation } from "../../../../lib/query"; + +import MutationButton from "../../../../components/form/mutation-button"; + +import useFormSubmit from "../../../../lib/form/submit"; +import { + useValue, + useTextInput, + useBoolInput, +} from "../../../../lib/form"; + +import { Checkbox, TextInput } from "../../../../components/form/inputs"; +import { AdminAccount } from "../../../../lib/types/account"; + +export interface AccountActionsProps { + account: AdminAccount, +} + +export function AccountActions({ account }: AccountActionsProps) { + const form = { + id: useValue("id", account.id), + reason: useTextInput("text") + }; + + const reallySuspend = useBoolInput("reallySuspend"); + const [accountAction, result] = useFormSubmit(form, useActionAccountMutation()); + + return ( +
+

Account Moderation Actions

+
+ Currently only the "suspend" action is implemented.
+ Suspending an account will delete it from your server, and remove all of its media, posts, relationships, etc.
+ If the suspended account is local, suspending will also send out a "delete" message to other servers, requesting them to remove its data from their instance as well.
+ Account suspension cannot be reversed. +
+ +
+ {/* + */} + + +
+ + ); +} diff --git a/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx b/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx new file mode 100644 index 000000000..5655421ea --- /dev/null +++ b/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx @@ -0,0 +1,118 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import React from "react"; +import { useLocation } from "wouter"; + +import { useHandleSignupMutation } from "../../../../lib/query"; + +import MutationButton from "../../../../components/form/mutation-button"; + +import useFormSubmit from "../../../../lib/form/submit"; +import { + useValue, + useTextInput, + useBoolInput, +} from "../../../../lib/form"; + +import { Checkbox, Select, TextInput } from "../../../../components/form/inputs"; +import { AdminAccount } from "../../../../lib/types/account"; + +export interface HandleSignupProps { + account: AdminAccount, + backLocation: string, +} + +export function HandleSignup({account, backLocation}: HandleSignupProps) { + const form = { + id: useValue("id", account.id), + approveOrReject: useTextInput("approve_or_reject", { defaultValue: "approve" }), + privateComment: useTextInput("private_comment"), + message: useTextInput("message"), + sendEmail: useBoolInput("send_email"), + }; + + const [_location, setLocation] = useLocation(); + + const [handleSignup, result] = useFormSubmit(form, useHandleSignupMutation(), { + changedOnly: false, + // After submitting the form, redirect back to + // /settings/admin/accounts if rejecting, since + // account will no longer be available at + // /settings/admin/accounts/:accountID endpoint. + onFinish: (res) => { + if (form.approveOrReject.value === "approve") { + // An approve request: + // stay on this page and + // serve updated details. + return; + } + + if (res.data) { + // "reject" successful, + // redirect to accounts page. + setLocation(backLocation); + } + } + }); + + return ( +
+

Handle Account Sign-Up

+ + { form.approveOrReject.value === "reject" && + // Only show form fields relevant + // to "reject" if rejecting. + // On "approve" these fields will + // be ignored anyway. + <> + + + + } + + + ); +} diff --git a/web/source/settings/views/moderation/accounts/detail/index.tsx b/web/source/settings/views/moderation/accounts/detail/index.tsx new file mode 100644 index 000000000..f507391d3 --- /dev/null +++ b/web/source/settings/views/moderation/accounts/detail/index.tsx @@ -0,0 +1,167 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import React from "react"; + +import { useGetAccountQuery } from "../../../../lib/query"; + +import FormWithData from "../../../../lib/form/form-with-data"; + +import FakeProfile from "../../../../components/fake-profile"; + +import { AdminAccount } from "../../../../lib/types/account"; +import { HandleSignup } from "./handlesignup"; +import { AccountActions } from "./actions"; +import { useParams } from "wouter"; + +export default function AccountDetail() { + const params: { accountID: string } = useParams(); + + return ( +
+

Account Details

+ +
+ ); +} + +interface AccountDetailFormProps { + backLocation: string, + data: AdminAccount, +} + +function AccountDetailForm({ data: adminAcct, backLocation }: AccountDetailFormProps) { + let yesOrNo = (b: boolean) => { + return b ? "yes" : "no"; + }; + + let created = new Date(adminAcct.created_at).toDateString(); + let lastPosted = "never"; + if (adminAcct.account.last_status_at) { + lastPosted = new Date(adminAcct.account.last_status_at).toDateString(); + } + const local = !adminAcct.domain; + + return ( + <> + +

General Account Details

+ { adminAcct.suspended && +
+ + Account is suspended. +
+ } +
+ { !local && +
+
Domain
+
{adminAcct.domain}
+
} +
+
Created
+
+
+
+
Last posted
+
{lastPosted}
+
+
+
Suspended
+
{yesOrNo(adminAcct.suspended)}
+
+
+
Silenced
+
{yesOrNo(adminAcct.silenced)}
+
+
+
Statuses
+
{adminAcct.account.statuses_count}
+
+
+
Followers
+
{adminAcct.account.followers_count}
+
+
+
Following
+
{adminAcct.account.following_count}
+
+
+ { local && + // Only show local account details + // if this is a local account! + <> +

Local Account Details

+ { !adminAcct.approved && +
+ + Account is pending. +
+ } + { !adminAcct.confirmed && +
+ + Account email not yet confirmed. +
+ } +
+
+
Email
+
{adminAcct.email} {{adminAcct.confirmed ? "(confirmed)" : "(not confirmed)"} }
+
+
+
Disabled
+
{yesOrNo(adminAcct.disabled)}
+
+
+
Approved
+
{yesOrNo(adminAcct.approved)}
+
+
+
Sign-Up Reason
+
{adminAcct.invite_request ?? none provided}
+
+ { (adminAcct.ip && adminAcct.ip !== "0.0.0.0") && +
+
Sign-Up IP
+
{adminAcct.ip}
+
} + { adminAcct.locale && +
+
Locale
+
{adminAcct.locale}
+
} +
+ } + { local && !adminAcct.approved + ? + + : + + } + + ); +} diff --git a/web/source/settings/views/moderation/accounts/index.tsx b/web/source/settings/views/moderation/accounts/index.tsx new file mode 100644 index 000000000..79ba2c674 --- /dev/null +++ b/web/source/settings/views/moderation/accounts/index.tsx @@ -0,0 +1,35 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import React from "react"; +import { AccountSearchForm } from "./search"; + +export default function AccountsOverview({ }) { + return ( +
+

Accounts Overview

+ + You can perform actions on an account by clicking + its name in a report, or by searching for the account + using the form below and clicking on its name. + + +
+ ); +} diff --git a/web/source/settings/views/moderation/accounts/pending/index.tsx b/web/source/settings/views/moderation/accounts/pending/index.tsx new file mode 100644 index 000000000..96b7796e5 --- /dev/null +++ b/web/source/settings/views/moderation/accounts/pending/index.tsx @@ -0,0 +1,40 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import React from "react"; +import { useSearchAccountsQuery } from "../../../../lib/query"; +import { AccountList } from "../../../../components/account-list"; + +export default function AccountsPending() { + const searchRes = useSearchAccountsQuery({status: "pending"}); + + return ( +
+

Pending Accounts

+ +
+ ); +} diff --git a/web/source/settings/views/moderation/accounts/search/index.tsx b/web/source/settings/views/moderation/accounts/search/index.tsx new file mode 100644 index 000000000..7d5515a43 --- /dev/null +++ b/web/source/settings/views/moderation/accounts/search/index.tsx @@ -0,0 +1,131 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import React from "react"; + +import { useLazySearchAccountsQuery } from "../../../../lib/query"; +import { useTextInput } from "../../../../lib/form"; + +import { AccountList } from "../../../../components/account-list"; +import { SearchAccountParams } from "../../../../lib/types/account"; +import { Select, TextInput } from "../../../../components/form/inputs"; +import MutationButton from "../../../../components/form/mutation-button"; + +export function AccountSearchForm() { + const form = { + origin: useTextInput("origin"), + status: useTextInput("status"), + permissions: useTextInput("permissions"), + username: useTextInput("username"), + display_name: useTextInput("display_name"), + by_domain: useTextInput("by_domain"), + email: useTextInput("email"), + ip: useTextInput("ip"), + }; + + function submitSearch(e) { + e.preventDefault(); + + // Parse query parameters. + const entries = Object.entries(form).map(([k, v]) => { + // Take only defined form fields. + if (v.value === undefined || v.value.length === 0) { + return null; + } + return [[k, v.value]]; + }).flatMap(kv => { + // Remove any nulls. + return kv || []; + }); + const params: SearchAccountParams = Object.fromEntries(entries); + searchAcct(params); + } + + const [ searchAcct, searchRes ] = useLazySearchAccountsQuery(); + + return ( + <> +
+ + + + + + + + + + + ); +} diff --git a/web/source/settings/views/moderation/domain-permissions/detail.tsx b/web/source/settings/views/moderation/domain-permissions/detail.tsx new file mode 100644 index 000000000..b9d439aee --- /dev/null +++ b/web/source/settings/views/moderation/domain-permissions/detail.tsx @@ -0,0 +1,262 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import React from "react"; + +import { useMemo } from "react"; +import { useLocation, useParams, useSearch } from "wouter"; + +import { useTextInput, useBoolInput } from "../../../lib/form"; + +import useFormSubmit from "../../../lib/form/submit"; + +import { TextInput, Checkbox, TextArea } from "../../../components/form/inputs"; + +import Loading from "../../../components/loading"; +import BackButton from "../../../components/back-button"; +import MutationButton from "../../../components/form/mutation-button"; + +import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../../lib/query/admin/domain-permissions/get"; +import { useAddDomainAllowMutation, useAddDomainBlockMutation, useRemoveDomainAllowMutation, useRemoveDomainBlockMutation } from "../../../lib/query/admin/domain-permissions/update"; +import { DomainPerm, PermType } from "../../../lib/types/domain-permission"; +import { NoArg } from "../../../lib/types/query"; +import { Error } from "../../../components/error"; +import { useBaseUrl } from "../../../lib/navigation/util"; + +export default function DomainPermDetail() { + const baseUrl = useBaseUrl(); + + // Parse perm type from routing params. + let params = useParams(); + if (params.permType !== "blocks" && params.permType !== "allows") { + throw "unrecognized perm type " + params.permType; + } + const permType = params.permType.slice(0, -1) as PermType; + + const { data: domainBlocks = {}, isLoading: isLoadingDomainBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" }); + const { data: domainAllows = {}, isLoading: isLoadingDomainAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" }); + + let isLoading; + switch (permType) { + case "block": + isLoading = isLoadingDomainBlocks; + break; + case "allow": + isLoading = isLoadingDomainAllows; + break; + default: + throw "perm type unknown"; + } + + // Parse domain from routing params. + let domain = params.domain ?? "unknown"; + + const search = useSearch(); + if (domain === "view") { + // Retrieve domain from form field submission. + const searchParams = new URLSearchParams(search); + const searchDomain = searchParams.get("domain"); + if (!searchDomain) { + throw "empty view domain"; + } + + domain = searchDomain; + } + + // Normalize / decode domain (it may be URL-encoded). + domain = decodeURIComponent(domain); + + // Check if we already have a perm of the desired type for this domain. + const existingPerm: DomainPerm | undefined = useMemo(() => { + if (permType == "block") { + return domainBlocks[domain]; + } else { + return domainAllows[domain]; + } + }, [domainBlocks, domainAllows, domain, permType]); + + let infoContent: React.JSX.Element; + + if (isLoading) { + infoContent = ; + } else if (existingPerm == undefined) { + infoContent = No stored {permType} yet, you can add one below:; + } else { + infoContent = ( +
+ + Editing domain permissions isn't implemented yet, check here for progress +
+ ); + } + + return ( +
+

Domain {permType} for: {domain}

+ {infoContent} + +
+ ); +} + +interface DomainPermFormProps { + defaultDomain: string; + perm?: DomainPerm; + permType: PermType; +} + +function DomainPermForm({ defaultDomain, perm, permType }: DomainPermFormProps) { + const isExistingPerm = perm !== undefined; + const disabledForm = isExistingPerm + ? { + disabled: true, + title: "Domain permissions currently cannot be edited." + } + : { + disabled: false, + title: "", + }; + + const form = { + domain: useTextInput("domain", { source: perm, defaultValue: defaultDomain }), + obfuscate: useBoolInput("obfuscate", { source: perm }), + commentPrivate: useTextInput("private_comment", { source: perm }), + commentPublic: useTextInput("public_comment", { source: perm }) + }; + + // Check which perm type we're meant to be handling + // here, and use appropriate mutations and results. + // We can't call these hooks conditionally because + // react is like "weh" (mood), but we can decide + // which ones to use conditionally. + const [ addBlock, addBlockResult ] = useAddDomainBlockMutation(); + const [ removeBlock, removeBlockResult] = useRemoveDomainBlockMutation({ fixedCacheKey: perm?.id }); + const [ addAllow, addAllowResult ] = useAddDomainAllowMutation(); + const [ removeAllow, removeAllowResult ] = useRemoveDomainAllowMutation({ fixedCacheKey: perm?.id }); + + const [ + addTrigger, + addResult, + removeTrigger, + removeResult, + ] = useMemo(() => { + return permType == "block" + ? [ + addBlock, + addBlockResult, + removeBlock, + removeBlockResult, + ] + : [ + addAllow, + addAllowResult, + removeAllow, + removeAllowResult, + ]; + }, [permType, + addBlock, addBlockResult, removeBlock, removeBlockResult, + addAllow, addAllowResult, removeAllow, removeAllowResult, + ]); + + // Use appropriate submission params for this permType. + const [submitForm, submitFormResult] = useFormSubmit(form, [addTrigger, addResult], { changedOnly: false }); + + // Uppercase first letter of given permType. + const permTypeUpper = useMemo(() => { + return permType.charAt(0).toUpperCase() + permType.slice(1); + }, [permType]); + + const [location, setLocation] = useLocation(); + + function verifyUrlThenSubmit(e) { + // Adding a new domain permissions happens on a url like + // "/settings/admin/domain-permissions/:permType/domain.com", + // but if domain input changes, that doesn't match anymore + // and causes issues later on so, before submitting the form, + // silently change url, and THEN submit. + let correctUrl = `/${permType}s/${form.domain.value}`; + if (location != correctUrl) { + setLocation(correctUrl); + } + return submitForm(e); + } + + return ( +
+ + + + +