diff options
Diffstat (limited to 'web/source/settings/views/moderation/accounts/detail')
4 files changed, 254 insertions, 186 deletions
diff --git a/web/source/settings/views/moderation/accounts/detail/actions.tsx b/web/source/settings/views/moderation/accounts/detail/actions.tsx index 212bb4089..4132b778a 100644 --- a/web/source/settings/views/moderation/accounts/detail/actions.tsx +++ b/web/source/settings/views/moderation/accounts/detail/actions.tsx @@ -19,7 +19,7 @@ import React from "react"; -import { useActionAccountMutation } from "../../../../lib/query/admin"; +import { useActionAccountMutation, useHandleSignupMutation } from "../../../../lib/query/admin"; import MutationButton from "../../../../components/form/mutation-button"; import useFormSubmit from "../../../../lib/form/submit"; import { @@ -27,22 +27,50 @@ import { useTextInput, useBoolInput, } from "../../../../lib/form"; -import { Checkbox, TextInput } from "../../../../components/form/inputs"; +import { Checkbox, Select, TextInput } from "../../../../components/form/inputs"; import { AdminAccount } from "../../../../lib/types/account"; +import { useLocation } from "wouter"; export interface AccountActionsProps { account: AdminAccount, + backLocation: string, } -export function AccountActions({ account }: AccountActionsProps) { +export function AccountActions({ account, backLocation }: AccountActionsProps) { + const local = !account.domain; + + // Available actions differ depending + // on the account's current status. + switch (true) { + case account.suspended: + // Can't do anything with + // suspended accounts currently. + return null; + case local && !account.approved: + // Unapproved local account sign-up, + // only show HandleSignup form. + return ( + <HandleSignup + account={account} + backLocation={backLocation} + /> + ); + default: + // Normal local or remote account, show + // full range of moderation options. + return <ModerateAccount account={account} />; + } +} + +function ModerateAccount({ account }: { account: AdminAccount }) { const form = { id: useValue("id", account.id), reason: useTextInput("text") }; - + const reallySuspend = useBoolInput("reallySuspend"); const [accountAction, result] = useFormSubmit(form, useActionAccountMutation()); - + return ( <form onSubmit={accountAction} @@ -60,16 +88,6 @@ export function AccountActions({ account }: AccountActionsProps) { placeholder="Reason for this action" /> <div className="action-buttons"> - {/* <MutationButton - label="Disable" - name="disable" - result={result} - /> - <MutationButton - label="Silence" - name="silence" - result={result} - /> */} <MutationButton disabled={account.suspended || reallySuspend.value === undefined || reallySuspend.value === false} label="Suspend" @@ -84,3 +102,81 @@ export function AccountActions({ account }: AccountActionsProps) { </form> ); } + +function HandleSignup({ account, backLocation }: { account: AdminAccount, backLocation: string }) { + 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 ( + <form + onSubmit={handleSignup} + aria-labelledby="account-handle-signup" + > + <h3 id="account-handle-signup">Handle Account Sign-Up</h3> + <Select + field={form.approveOrReject} + label="Approve or Reject" + options={ + <> + <option value="approve">Approve</option> + <option value="reject">Reject</option> + </> + } + > + </Select> + { form.approveOrReject.value === "reject" && + // Only show form fields relevant + // to "reject" if rejecting. + // On "approve" these fields will + // be ignored anyway. + <> + <TextInput + field={form.privateComment} + label="(Optional) private comment on why sign-up was rejected (shown to other admins only)" + /> + <Checkbox + field={form.sendEmail} + label="Send email to applicant" + /> + <TextInput + field={form.message} + label={"(Optional) message to include in email to applicant, if send email is checked"} + /> + </> } + <MutationButton + disabled={false} + label={form.approveOrReject.value === "approve" ? "Approve" : "Reject"} + result={result} + /> + </form> + ); +} diff --git a/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx b/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx deleted file mode 100644 index 59fa8bc65..000000000 --- a/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - 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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import { useLocation } from "wouter"; -import { useHandleSignupMutation } from "../../../../lib/query/admin"; -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 ( - <form - onSubmit={handleSignup} - aria-labelledby="account-handle-signup" - > - <h3 id="account-handle-signup">Handle Account Sign-Up</h3> - <Select - field={form.approveOrReject} - label="Approve or Reject" - options={ - <> - <option value="approve">Approve</option> - <option value="reject">Reject</option> - </> - } - > - </Select> - { form.approveOrReject.value === "reject" && - // Only show form fields relevant - // to "reject" if rejecting. - // On "approve" these fields will - // be ignored anyway. - <> - <TextInput - field={form.privateComment} - label="(Optional) private comment on why sign-up was rejected (shown to other admins only)" - /> - <Checkbox - field={form.sendEmail} - label="Send email to applicant" - /> - <TextInput - field={form.message} - label={"(Optional) message to include in email to applicant, if send email is checked"} - /> - </> } - <MutationButton - disabled={false} - label={form.approveOrReject.value === "approve" ? "Approve" : "Reject"} - result={result} - /> - </form> - ); -} diff --git a/web/source/settings/views/moderation/accounts/detail/index.tsx b/web/source/settings/views/moderation/accounts/detail/index.tsx index f34bc7481..830a894cb 100644 --- a/web/source/settings/views/moderation/accounts/detail/index.tsx +++ b/web/source/settings/views/moderation/accounts/detail/index.tsx @@ -23,51 +23,89 @@ import { useGetAccountQuery } from "../../../../lib/query/admin"; 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"; +import { useBaseUrl } from "../../../../lib/navigation/util"; +import BackButton from "../../../../components/back-button"; +import { UseOurInstanceAccount, yesOrNo } from "./util"; export default function AccountDetail() { const params: { accountID: string } = useParams(); - + const baseUrl = useBaseUrl(); + const backLocation: String = history.state?.backLocation ?? `~${baseUrl}`; + return ( <div className="account-detail"> - <h1>Account Details</h1> + <h1><BackButton to={backLocation} /> Account Details</h1> <FormWithData dataQuery={useGetAccountQuery} queryArg={params.accountID} DataForm={AccountDetailForm} + {...{ backLocation: backLocation }} /> </div> ); } interface AccountDetailFormProps { - backLocation: string, - data: AdminAccount, + data: AdminAccount; + backLocation: string; } -function AccountDetailForm({ data: adminAcct, backLocation }: AccountDetailFormProps) { - let yesOrNo = (b: boolean) => { - return b ? "yes" : "no"; - }; +function AccountDetailForm({ data: adminAcct, backLocation }: AccountDetailFormProps) { + // If this is our instance account, don't + // bother returning detailed account information. + const ourInstanceAccount = UseOurInstanceAccount(adminAcct); + if (ourInstanceAccount) { + return ( + <> + <FakeProfile {...adminAcct.account} /> + <div className="info"> + <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i> + <b> + This is the service account for your instance; you + cannot perform moderation actions on this account. + </b> + </div> + </> + ); + } + + const local = !adminAcct.domain; + return ( + <> + <FakeProfile {...adminAcct.account} /> + <GeneralAccountDetails adminAcct={adminAcct} /> + { + // Only show local account details + // if this is a local account! + local && <LocalAccountDetails adminAcct={adminAcct} /> + } + <AccountActions + account={adminAcct} + backLocation={backLocation} + /> + </> + ); +} - let created = new Date(adminAcct.created_at).toDateString(); +function GeneralAccountDetails({ adminAcct } : { adminAcct: AdminAccount }) { + const local = !adminAcct.domain; + const 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 ( <> - <FakeProfile {...adminAcct.account} /> <h3>General Account Details</h3> - { adminAcct.suspended && - <div className="info"> - <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i> - <b>Account is suspended.</b> - </div> + { adminAcct.suspended && + <div className="info"> + <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i> + <b>Account is suspended.</b> + </div> } <dl className="info-list"> { !local && @@ -76,6 +114,18 @@ function AccountDetailForm({ data: adminAcct, backLocation }: AccountDetailFormP <dd>{adminAcct.domain}</dd> </div>} <div className="info-list-entry"> + <dt>Profile URL</dt> + <dd> + <a + href={adminAcct.account.url} + target="_blank" + rel="noreferrer" + > + <i className="fa fa-fw fa-external-link" aria-hidden="true"></i> {adminAcct.account.url} (opens in a new tab) + </a> + </dd> + </div> + <div className="info-list-entry"> <dt>Created</dt> <dd><time dateTime={adminAcct.created_at}>{created}</time></dd> </div> @@ -104,61 +154,54 @@ function AccountDetailForm({ data: adminAcct, backLocation }: AccountDetailFormP <dd>{adminAcct.account.following_count}</dd> </div> </dl> - { local && - // Only show local account details - // if this is a local account! - <> - <h3>Local Account Details</h3> - { !adminAcct.approved && + </> + ); +} + +function LocalAccountDetails({ adminAcct }: { adminAcct: AdminAccount }) { + return ( + <> + <h3>Local Account Details</h3> + { !adminAcct.approved && <div className="info"> <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i> <b>Account is pending.</b> </div> - } - { !adminAcct.confirmed && + } + { !adminAcct.confirmed && <div className="info"> <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i> <b>Account email not yet confirmed.</b> </div> - } - <dl className="info-list"> - <div className="info-list-entry"> - <dt>Email</dt> - <dd>{adminAcct.email} {<b>{adminAcct.confirmed ? "(confirmed)" : "(not confirmed)"}</b> }</dd> - </div> - <div className="info-list-entry"> - <dt>Disabled</dt> - <dd>{yesOrNo(adminAcct.disabled)}</dd> - </div> - <div className="info-list-entry"> - <dt>Approved</dt> - <dd>{yesOrNo(adminAcct.approved)}</dd> - </div> - <div className="info-list-entry"> - <dt>Sign-Up Reason</dt> - <dd>{adminAcct.invite_request ?? <i>none provided</i>}</dd> - </div> - { (adminAcct.ip && adminAcct.ip !== "0.0.0.0") && + } + <dl className="info-list"> + <div className="info-list-entry"> + <dt>Email</dt> + <dd>{adminAcct.email} {<b>{adminAcct.confirmed ? "(confirmed)" : "(not confirmed)"}</b> }</dd> + </div> + <div className="info-list-entry"> + <dt>Disabled</dt> + <dd>{yesOrNo(adminAcct.disabled)}</dd> + </div> + <div className="info-list-entry"> + <dt>Approved</dt> + <dd>{yesOrNo(adminAcct.approved)}</dd> + </div> + <div className="info-list-entry"> + <dt>Sign-Up Reason</dt> + <dd>{adminAcct.invite_request ?? <i>none provided</i>}</dd> + </div> + { (adminAcct.ip && adminAcct.ip !== "0.0.0.0") && <div className="info-list-entry"> <dt>Sign-Up IP</dt> <dd>{adminAcct.ip}</dd> </div> } - { adminAcct.locale && + { adminAcct.locale && <div className="info-list-entry"> <dt>Locale</dt> <dd>{adminAcct.locale}</dd> </div> } - </dl> - </> } - { local && !adminAcct.approved - ? - <HandleSignup - account={adminAcct} - backLocation={backLocation} - /> - : - <AccountActions account={adminAcct} /> - } - </> + </dl> + </> ); } diff --git a/web/source/settings/views/moderation/accounts/detail/util.tsx b/web/source/settings/views/moderation/accounts/detail/util.tsx new file mode 100644 index 000000000..b82d44a6e --- /dev/null +++ b/web/source/settings/views/moderation/accounts/detail/util.tsx @@ -0,0 +1,43 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +import { useMemo } from "react"; + +import { AdminAccount } from "../../../../lib/types/account"; +import { store } from "../../../../redux/store"; + +export function yesOrNo(b: boolean): string { + return b ? "yes" : "no"; +} + +export function UseOurInstanceAccount(account: AdminAccount): boolean { + // Pull our own URL out of storage so we can + // tell if account is our instance account. + const ourDomain = useMemo(() => { + const instanceUrlStr = store.getState().oauth.instanceUrl; + if (!instanceUrlStr) { + return ""; + } + + const instanceUrl = new URL(instanceUrlStr); + return instanceUrl.host; + }, []); + + return !account.domain && account.username == ourDomain; +} |