[feature] Admin accounts endpoints; approve/reject sign-ups (#2826)

* update settings panels, add pending overview + approve/deny functions

* add admin accounts get, approve, reject

* send approved/rejected emails

* use signup URL

* docs!

* email

* swagger

* web linting

* fix email tests

* wee lil fixerinos

* use new paging logic for GetAccounts() series of admin endpoints, small changes to query building

* shuffle useAccountIDIn check *before* adding to query

* fix parse from toot react error

* use `netip.Addr`

* put valid slices in globals

* optimistic updates for account state

---------

Co-authored-by: kim <grufwub@gmail.com>
This commit is contained in:
tobi
2024-04-13 13:25:10 +02:00
committed by GitHub
parent 1439042104
commit 89e0cfd874
74 changed files with 4102 additions and 545 deletions

View File

@@ -130,10 +130,11 @@ main {
}
}
&:disabled {
&:disabled,
&.disabled {
color: $white2;
background: $gray2;
cursor: auto;
cursor: not-allowed;
&:hover {
background: $gray3;

View File

@@ -1,112 +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/>.
*/
const React = require("react");
const { useRoute, Redirect } = require("wouter");
const query = require("../../lib/query");
const FormWithData = require("../../lib/form/form-with-data").default;
const { useBaseUrl } = require("../../lib/navigation/util");
const FakeProfile = require("../../components/fake-profile");
const MutationButton = require("../../components/form/mutation-button");
const useFormSubmit = require("../../lib/form/submit").default;
const { useValue, useTextInput } = require("../../lib/form");
const { TextInput } = require("../../components/form/inputs");
module.exports = function AccountDetail({ }) {
const baseUrl = useBaseUrl();
let [_match, params] = useRoute(`${baseUrl}/:accountId`);
if (params?.accountId == undefined) {
return <Redirect to={baseUrl} />;
} else {
return (
<div className="account-detail">
<h1>
Account Details
</h1>
<FormWithData
dataQuery={query.useGetAccountQuery}
queryArg={params.accountId}
DataForm={AccountDetailForm}
/>
</div>
);
}
};
function AccountDetailForm({ data: account }) {
let content;
if (account.suspended) {
content = (
<h2 className="error">Account is suspended.</h2>
);
} else {
content = <ModifyAccount account={account} />;
}
return (
<>
<FakeProfile {...account} />
{content}
</>
);
}
function ModifyAccount({ account }) {
const form = {
id: useValue("id", account.id),
reason: useTextInput("text")
};
const [modifyAccount, result] = useFormSubmit(form, query.useActionAccountMutation());
return (
<form onSubmit={modifyAccount}>
<h2>Actions</h2>
<TextInput
field={form.reason}
placeholder="Reason for this action"
/>
<div className="action-buttons">
{/* <MutationButton
label="Disable"
name="disable"
result={result}
/>
<MutationButton
label="Silence"
name="silence"
result={result}
/> */}
<MutationButton
label="Suspend"
name="suspend"
result={result}
/>
</div>
</form>
);
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 (
<form
onSubmit={accountAction}
aria-labelledby="account-moderation-actions"
>
<h3 id="account-moderation-actions">Account Moderation Actions</h3>
<div>
Currently only the "suspend" action is implemented.<br/>
Suspending an account will delete it from your server, and remove all of its media, posts, relationships, etc.<br/>
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.<br/>
<b>Account suspension cannot be reversed.</b>
</div>
<TextInput
field={form.reason}
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"
name="suspend"
result={result}
/>
<Checkbox
label="Really suspend"
field={reallySuspend}
></Checkbox>
</div>
</form>
);
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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,
accountsBaseUrl: string,
}
export function HandleSignup({account, accountsBaseUrl}: 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(accountsBaseUrl);
}
}
});
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>
);
}

View File

@@ -0,0 +1,179 @@
/*
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 { useRoute, Redirect } from "wouter";
import { useGetAccountQuery } from "../../../lib/query";
import FormWithData from "../../../lib/form/form-with-data";
import { useBaseUrl } from "../../../lib/navigation/util";
import FakeProfile from "../../../components/fake-profile";
import { AdminAccount } from "../../../lib/types/account";
import { HandleSignup } from "./handlesignup";
import { AccountActions } from "./actions";
import BackButton from "../../../components/back-button";
export default function AccountDetail() {
// /settings/admin/accounts
const accountsBaseUrl = useBaseUrl();
let [_match, params] = useRoute(`${accountsBaseUrl}/:accountId`);
if (params?.accountId == undefined) {
return <Redirect to={accountsBaseUrl} />;
} else {
return (
<div className="account-detail">
<h1 className="text-cutoff">
<BackButton to={accountsBaseUrl} /> Account Details
</h1>
<FormWithData
dataQuery={useGetAccountQuery}
queryArg={params.accountId}
DataForm={AccountDetailForm}
{...{accountsBaseUrl}}
/>
</div>
);
}
}
interface AccountDetailFormProps {
accountsBaseUrl: string,
data: AdminAccount,
}
function AccountDetailForm({ data: adminAcct, accountsBaseUrl }: 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 (
<>
<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>
}
<dl className="info-list">
{ !local &&
<div className="info-list-entry">
<dt>Domain</dt>
<dd>{adminAcct.domain}</dd>
</div>}
<div className="info-list-entry">
<dt>Created</dt>
<dd><time dateTime={adminAcct.created_at}>{created}</time></dd>
</div>
<div className="info-list-entry">
<dt>Last posted</dt>
<dd>{lastPosted}</dd>
</div>
<div className="info-list-entry">
<dt>Suspended</dt>
<dd>{yesOrNo(adminAcct.suspended)}</dd>
</div>
<div className="info-list-entry">
<dt>Silenced</dt>
<dd>{yesOrNo(adminAcct.silenced)}</dd>
</div>
<div className="info-list-entry">
<dt>Statuses</dt>
<dd>{adminAcct.account.statuses_count}</dd>
</div>
<div className="info-list-entry">
<dt>Followers</dt>
<dd>{adminAcct.account.followers_count}</dd>
</div>
<div className="info-list-entry">
<dt>Following</dt>
<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 &&
<div className="info">
<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
<b>Account is pending.</b>
</div>
}
{ !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") &&
<div className="info-list-entry">
<dt>Sign-Up IP</dt>
<dd>{adminAcct.ip}</dd>
</div> }
{ adminAcct.locale &&
<div className="info-list-entry">
<dt>Locale</dt>
<dd>{adminAcct.locale}</dd>
</div> }
</dl>
</> }
{ local && !adminAcct.approved
?
<HandleSignup
account={adminAcct}
accountsBaseUrl={accountsBaseUrl}
/>
:
<AccountActions account={adminAcct} />
}
</>
);
}

View File

@@ -1,138 +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/>.
*/
const React = require("react");
const { Switch, Route, Link } = require("wouter");
const query = require("../../lib/query");
const { useTextInput } = require("../../lib/form");
const AccountDetail = require("./detail");
const { useBaseUrl } = require("../../lib/navigation/util");
const { Error } = require("../../components/error");
module.exports = function Accounts({ baseUrl }) {
return (
<div className="accounts">
<Switch>
<Route path={`${baseUrl}/:accountId`}>
<AccountDetail />
</Route>
<AccountOverview />
</Switch>
</div>
);
};
function AccountOverview({ }) {
return (
<>
<h1>Accounts</h1>
<div>
Pending <a href="https://github.com/superseriousbusiness/gotosocial/issues/581">#581</a>,
there is currently no way to list accounts.<br />
You can perform actions on reported accounts by clicking their name in the report, or searching for a username below.
</div>
<AccountSearchForm />
</>
);
}
function AccountSearchForm() {
const [searchAccount, result] = query.useSearchAccountMutation();
const [onAccountChange, _resetAccount, { account }] = useTextInput("account");
function submitSearch(e) {
e.preventDefault();
if (account.trim().length != 0) {
searchAccount(account);
}
}
return (
<div className="account-search">
<form onSubmit={submitSearch}>
<div className="form-field text">
<label htmlFor="url">
Account:
</label>
<div className="row">
<input
type="text"
id="account"
name="account"
onChange={onAccountChange}
value={account}
/>
<button disabled={result.isLoading}>
<i className={[
"fa fa-fw",
(result.isLoading
? "fa-refresh fa-spin"
: "fa-search")
].join(" ")} aria-hidden="true" title="Search" />
<span className="sr-only">Search</span>
</button>
</div>
</div>
</form>
<AccountList
isSuccess={result.isSuccess}
data={result.data}
isError={result.isError}
error={result.error}
/>
</div>
);
}
function AccountList({ isSuccess, data, isError, error }) {
const baseUrl = useBaseUrl();
if (!(isSuccess || isError)) {
return null;
}
if (error) {
return <Error error={error} />;
}
if (data.length == 0) {
return <b>No accounts found that match your query</b>;
}
return (
<>
<h2>Results:</h2>
<div className="list">
{data.map((acc) => (
<Link key={acc.acct} className="account entry" to={`${baseUrl}/${acc.id}`}>
{acc.display_name?.length > 0
? acc.display_name
: acc.username
}
<span id="username">(@{acc.acct})</span>
</Link>
))}
</div>
</>
);
}

View File

@@ -0,0 +1,49 @@
/*
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 { Switch, Route } from "wouter";
import AccountDetail from "./detail";
import { AccountSearchForm } from "./search";
export default function Accounts({ baseUrl }) {
return (
<Switch>
<Route path={`${baseUrl}/:accountId`}>
<AccountDetail />
</Route>
<AccountOverview />
</Switch>
);
}
function AccountOverview({ }) {
return (
<div className="accounts-view">
<h1>Accounts Overview</h1>
<span>
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.
</span>
<AccountSearchForm />
</div>
);
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 (
<div className="accounts-view">
<h1>Pending Accounts</h1>
<AccountList
isLoading={searchRes.isLoading}
isSuccess={searchRes.isSuccess}
data={searchRes.data}
isError={searchRes.isError}
error={searchRes.error}
emptyMessage="No pending account sign-ups."
/>
</div>
);
}

View File

@@ -0,0 +1,125 @@
/*
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 { 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 [searchAcct, searchRes] = useLazySearchAccountsQuery();
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);
}
return (
<>
<form onSubmit={submitSearch}>
<TextInput
field={form.username}
label={"(Optional) username (without leading '@' symbol)"}
placeholder="someone"
/>
<TextInput
field={form.by_domain}
label={"(Optional) domain"}
placeholder="example.org"
/>
<Select
field={form.origin}
label="Account origin"
options={
<>
<option value="">Local or remote</option>
<option value="local">Local only</option>
<option value="remote">Remote only</option>
</>
}
></Select>
<TextInput
field={form.email}
label={"(Optional) email address (local accounts only)"}
placeholder={"someone@example.org"}
/>
<TextInput
field={form.ip}
label={"(Optional) IP address (local accounts only)"}
placeholder={"198.51.100.0"}
/>
<Select
field={form.status}
label="Account status"
options={
<>
<option value="">Any</option>
<option value="pending">Pending only</option>
<option value="disabled">Disabled only</option>
<option value="suspended">Suspended only</option>
</>
}
></Select>
<MutationButton
disabled={false}
label={"Search"}
result={searchRes}
/>
</form>
<AccountList
isLoading={searchRes.isLoading}
isSuccess={searchRes.isSuccess}
data={searchRes.data}
isError={searchRes.isError}
error={searchRes.error}
emptyMessage="No accounts found that match your query"
/>
</>
);
}

View File

@@ -17,19 +17,19 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React from "react";
const query = require("../../../lib/query");
import { useInstanceKeysExpireMutation } from "../../../lib/query";
const { useTextInput } = require("../../../lib/form");
const { TextInput } = require("../../../components/form/inputs");
import { useTextInput } from "../../../lib/form";
import { TextInput } from "../../../components/form/inputs";
const MutationButton = require("../../../components/form/mutation-button");
import MutationButton from "../../../components/form/mutation-button";
module.exports = function ExpireRemote({}) {
export default function ExpireRemote({}) {
const domainField = useTextInput("domain");
const [expire, expireResult] = query.useInstanceKeysExpireMutation();
const [expire, expireResult] = useInstanceKeysExpireMutation();
function submitExpire(e) {
e.preventDefault();
@@ -53,7 +53,11 @@ module.exports = function ExpireRemote({}) {
type="string"
placeholder="example.org"
/>
<MutationButton label="Expire keys" result={expireResult} />
<MutationButton
disabled={false}
label="Expire keys"
result={expireResult}
/>
</form>
);
};
}

View File

@@ -17,14 +17,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const ExpireRemote = require("./expireremote");
import React from "react";
import ExpireRemote from "./expireremote";
module.exports = function Keys() {
export default function Keys() {
return (
<>
<h1>Key Actions</h1>
<ExpireRemote />
</>
);
};
}

View File

@@ -17,19 +17,19 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React from "react";
const query = require("../../../lib/query");
import { useMediaCleanupMutation } from "../../../lib/query";
const { useTextInput } = require("../../../lib/form");
const { TextInput } = require("../../../components/form/inputs");
import { useTextInput } from "../../../lib/form";
import { TextInput } from "../../../components/form/inputs";
const MutationButton = require("../../../components/form/mutation-button");
import MutationButton from "../../../components/form/mutation-button";
module.exports = function Cleanup({}) {
const daysField = useTextInput("days", { defaultValue: 30 });
export default function Cleanup({}) {
const daysField = useTextInput("days", { defaultValue: "30" });
const [mediaCleanup, mediaCleanupResult] = query.useMediaCleanupMutation();
const [mediaCleanup, mediaCleanupResult] = useMediaCleanupMutation();
function submitCleanup(e) {
e.preventDefault();
@@ -51,7 +51,11 @@ module.exports = function Cleanup({}) {
min="0"
placeholder="30"
/>
<MutationButton label="Remove old media" result={mediaCleanupResult} />
<MutationButton
disabled={false}
label="Remove old media"
result={mediaCleanupResult}
/>
</form>
);
};
}

View File

@@ -17,14 +17,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const Cleanup = require("./cleanup");
import React from "react";
import Cleanup from "./cleanup";
module.exports = function Media() {
export default function Media() {
return (
<>
<h1>Media Actions</h1>
<Cleanup />
</>
);
};
}

View File

@@ -100,9 +100,9 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
onClick={() => submitParse()}
result={parseResult}
showError={false}
disabled={false}
disabled={form.permType.value === undefined || form.permType.value.length === 0}
/>
<label className="button with-icon">
<label className={`button with-icon${form.permType.value === undefined || form.permType.value.length === 0 ? " disabled" : ""}`}>
<i className="fa fa-fw " aria-hidden="true" />
Import file
<input
@@ -110,6 +110,7 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
className="hidden"
onChange={fileChanged}
accept="application/json,text/plain,text/csv"
disabled={form.permType.value === undefined || form.permType.value.length === 0}
/>
</label>
<b /> {/* grid filler */}
@@ -118,7 +119,7 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
type="button"
onClick={() => submitExport("export")}
result={exportResult} showError={false}
disabled={false}
disabled={form.permType.value === undefined || form.permType.value.length === 0}
/>
<MutationButton
label="Export to file"
@@ -127,7 +128,7 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
onClick={() => submitExport("export-file")}
result={exportResult}
showError={false}
disabled={false}
disabled={form.permType.value === undefined || form.permType.value.length === 0}
/>
<div className="export-file">
<span>

View File

@@ -17,29 +17,25 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { useRoute, Link, Redirect } = require("wouter");
import React, { useEffect } from "react";
import { useRoute, Link, Redirect } from "wouter";
const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form");
const { CategorySelect } = require("../category-select");
import { useComboBoxInput, useFileInput, useValue } from "../../../lib/form";
import { CategorySelect } from "../category-select";
const useFormSubmit = require("../../../lib/form/submit").default;
const { useBaseUrl } = require("../../../lib/navigation/util");
import useFormSubmit from "../../../lib/form/submit";
import { useBaseUrl } from "../../../lib/navigation/util";
const FakeToot = require("../../../components/fake-toot");
const FormWithData = require("../../../lib/form/form-with-data").default;
const Loading = require("../../../components/loading");
const { FileInput } = require("../../../components/form/inputs");
const MutationButton = require("../../../components/form/mutation-button");
const { Error } = require("../../../components/error");
import FakeToot from "../../../components/fake-toot";
import FormWithData from "../../../lib/form/form-with-data";
import Loading from "../../../components/loading";
import { FileInput } from "../../../components/form/inputs";
import MutationButton from "../../../components/form/mutation-button";
import { Error } from "../../../components/error";
const {
useGetEmojiQuery,
useEditEmojiMutation,
useDeleteEmojiMutation,
} = require("../../../lib/query/admin/custom-emoji");
import { useGetEmojiQuery, useEditEmojiMutation, useDeleteEmojiMutation } from "../../../lib/query/admin/custom-emoji";
module.exports = function EmojiDetailRoute({ }) {
export default function EmojiDetailRoute({ }) {
const baseUrl = useBaseUrl();
let [_match, params] = useRoute(`${baseUrl}/:emojiId`);
if (params?.emojiId == undefined) {
@@ -52,7 +48,7 @@ module.exports = function EmojiDetailRoute({ }) {
</div>
);
}
};
}
function EmojiDetailForm({ data: emoji }) {
const baseUrl = useBaseUrl();
@@ -68,7 +64,7 @@ function EmojiDetailForm({ data: emoji }) {
const [modifyEmoji, result] = useFormSubmit(form, useEditEmojiMutation());
// Automatic submitting of category change
React.useEffect(() => {
useEffect(() => {
if (
form.category.hasChanged() &&
!form.category.state.open &&

View File

@@ -17,13 +17,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { Switch, Route } = require("wouter");
import React from "react";
import { Switch, Route } from "wouter";
const EmojiOverview = require("./overview");
const EmojiDetail = require("./detail");
import EmojiOverview from "./overview";
import EmojiDetail from "./detail";
module.exports = function CustomEmoji({ baseUrl }) {
export default function CustomEmoji({ baseUrl }) {
return (
<Switch>
<Route path={`${baseUrl}/:emojiId`}>
@@ -32,4 +32,4 @@ module.exports = function CustomEmoji({ baseUrl }) {
<EmojiOverview />
</Switch>
);
};
}

View File

@@ -17,31 +17,26 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React, { useMemo, useEffect } from "react";
const {
useFileInput,
useComboBoxInput
} = require("../../../lib/form");
const useShortcode = require("./use-shortcode");
import { useFileInput, useComboBoxInput } from "../../../lib/form";
import useShortcode from "./use-shortcode";
const useFormSubmit = require("../../../lib/form/submit").default;
import useFormSubmit from "../../../lib/form/submit";
const {
TextInput, FileInput
} = require("../../../components/form/inputs");
import { TextInput, FileInput } from "../../../components/form/inputs";
const { CategorySelect } = require('../category-select');
const FakeToot = require("../../../components/fake-toot");
const MutationButton = require("../../../components/form/mutation-button");
const { useAddEmojiMutation } = require("../../../lib/query/admin/custom-emoji");
const { useInstanceV1Query } = require("../../../lib/query");
import { CategorySelect } from '../category-select';
import FakeToot from "../../../components/fake-toot";
import MutationButton from "../../../components/form/mutation-button";
import { useAddEmojiMutation } from "../../../lib/query/admin/custom-emoji";
import { useInstanceV1Query } from "../../../lib/query";
module.exports = function NewEmojiForm() {
export default function NewEmojiForm() {
const shortcode = useShortcode();
const { data: instance } = useInstanceV1Query();
const emojiMaxSize = React.useMemo(() => {
const emojiMaxSize = useMemo(() => {
return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024;
}, [instance]);
@@ -56,8 +51,8 @@ module.exports = function NewEmojiForm() {
shortcode, image, category
}, useAddEmojiMutation());
React.useEffect(() => {
if (shortcode.value.length == 0) {
useEffect(() => {
if (shortcode.value === undefined || shortcode.value.length == 0) {
if (image.value != undefined) {
let [name, _ext] = image.value.name.split(".");
shortcode.setter(name);
@@ -71,7 +66,7 @@ module.exports = function NewEmojiForm() {
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [image.value]);
let emojiOrShortcode = `:${shortcode.value}:`;
let emojiOrShortcode;
if (image.previewValue != undefined) {
emojiOrShortcode = <img
@@ -80,6 +75,10 @@ module.exports = function NewEmojiForm() {
title={`:${shortcode.value}:`}
alt={shortcode.value}
/>;
} else if (shortcode.value !== undefined && shortcode.value.length > 0) {
emojiOrShortcode = `:${shortcode.value}:`;
} else {
emojiOrShortcode = `:your_emoji_here:`;
}
return (
@@ -103,10 +102,15 @@ module.exports = function NewEmojiForm() {
<CategorySelect
field={category}
children={[]}
/>
<MutationButton label="Upload emoji" result={result} />
<MutationButton
disabled={image.previewValue === undefined}
label="Upload emoji"
result={result}
/>
</form>
</div>
);
};
}

View File

@@ -22,7 +22,7 @@ const { Link } = require("wouter");
const syncpipe = require("syncpipe");
const { matchSorter } = require("match-sorter");
const NewEmojiForm = require("./new-emoji");
const NewEmojiForm = require("./new-emoji").default;
const { useTextInput } = require("../../../lib/form");
const { useEmojiByCategory } = require("../category-select");

View File

@@ -17,15 +17,15 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React, { useMemo } from "react";
const ParseFromToot = require("./parse-from-toot");
import ParseFromToot from "./parse-from-toot";
const Loading = require("../../../components/loading");
const { Error } = require("../../../components/error");
const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
import Loading from "../../../components/loading";
import { Error } from "../../../components/error";
import { useListEmojiQuery } from "../../../lib/query/admin/custom-emoji";
module.exports = function RemoteEmoji() {
export default function RemoteEmoji() {
// local emoji are queried for shortcode collision detection
const {
data: emoji = [],
@@ -33,7 +33,7 @@ module.exports = function RemoteEmoji() {
error
} = useListEmojiQuery({ filter: "domain:local" });
const emojiCodes = React.useMemo(() => {
const emojiCodes = useMemo(() => {
return new Set(emoji.map((e) => e.shortcode));
}, [emoji]);
@@ -46,9 +46,9 @@ module.exports = function RemoteEmoji() {
{isLoading
? <Loading />
: <>
<ParseFromToot emoji={emoji} emojiCodes={emojiCodes} />
<ParseFromToot emojiCodes={emojiCodes} />
</>
}
</>
);
};
}

View File

@@ -17,36 +17,28 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React, { useCallback, useEffect } from "react";
const {
useTextInput,
useComboBoxInput,
useCheckListInput
} = require("../../../lib/form");
import { useTextInput, useComboBoxInput, useCheckListInput } from "../../../lib/form";
const useFormSubmit = require("../../../lib/form/submit").default;
import useFormSubmit from "../../../lib/form/submit";
const CheckList = require("../../../components/check-list").default;
const { CategorySelect } = require('../category-select');
import CheckList from "../../../components/check-list";
import { CategorySelect } from '../category-select';
const { TextInput } = require("../../../components/form/inputs");
const MutationButton = require("../../../components/form/mutation-button");
const { Error } = require("../../../components/error");
const {
useSearchItemForEmojiMutation,
usePatchRemoteEmojisMutation
} = require("../../../lib/query/admin/custom-emoji");
import { TextInput } from "../../../components/form/inputs";
import MutationButton from "../../../components/form/mutation-button";
import { Error } from "../../../components/error";
import { useSearchItemForEmojiMutation, usePatchRemoteEmojisMutation } from "../../../lib/query/admin/custom-emoji";
module.exports = function ParseFromToot({ emojiCodes }) {
export default function ParseFromToot({ emojiCodes }) {
const [searchStatus, result] = useSearchItemForEmojiMutation();
const [onURLChange, _resetURL, { url }] = useTextInput("url");
const urlField = useTextInput("url");
function submitSearch(e) {
e.preventDefault();
if (url.trim().length != 0) {
searchStatus(url);
if (urlField.value !== undefined && urlField.value.trim().length != 0) {
searchStatus(urlField.value);
}
}
@@ -63,8 +55,8 @@ module.exports = function ParseFromToot({ emojiCodes }) {
type="text"
id="url"
name="url"
onChange={onURLChange}
value={url}
onChange={urlField.onChange}
value={urlField.value}
/>
<button disabled={result.isLoading}>
<i className={[
@@ -81,7 +73,7 @@ module.exports = function ParseFromToot({ emojiCodes }) {
<SearchResult result={result} localEmojiCodes={emojiCodes} />
</div>
);
};
}
function SearchResult({ result, localEmojiCodes }) {
const { error, data, isSuccess, isError } = result;
@@ -106,7 +98,6 @@ function SearchResult({ result, localEmojiCodes }) {
<CopyEmojiForm
localEmojiCodes={localEmojiCodes}
type={data.type}
domain={data.domain}
emojiList={data.list}
/>
);
@@ -139,13 +130,16 @@ function CopyEmojiForm({ localEmojiCodes, type, emojiList }) {
);
const buttonsInactive = form.selectedEmoji.someSelected
? {}
? {
disabled: false,
title: ""
}
: {
disabled: true,
title: "No emoji selected, cannot perform any actions"
};
const checkListExtraProps = React.useCallback(() => ({ localEmojiCodes }), [localEmojiCodes]);
const checkListExtraProps = useCallback(() => ({ localEmojiCodes }), [localEmojiCodes]);
return (
<div className="parsed">
@@ -153,17 +147,32 @@ function CopyEmojiForm({ localEmojiCodes, type, emojiList }) {
<form onSubmit={formSubmit}>
<CheckList
field={form.selectedEmoji}
header={<></>}
EntryComponent={EmojiEntry}
getExtraProps={checkListExtraProps}
/>
<CategorySelect
field={form.category}
children={[]}
/>
<div className="action-buttons row">
<MutationButton name="copy" label="Copy to local emoji" result={result} showError={false} {...buttonsInactive} />
<MutationButton name="disable" label="Disable" result={result} className="button danger" showError={false} {...buttonsInactive} />
<MutationButton
name="copy"
label="Copy to local emoji"
result={result}
showError={false}
{...buttonsInactive}
/>
<MutationButton
name="disable"
label="Disable"
result={result}
className="button danger"
showError={false}
{...buttonsInactive}
/>
</div>
{result.error && (
Array.isArray(result.error)
@@ -198,13 +207,13 @@ function EmojiEntry({ entry: emoji, onChange, extraProps: { localEmojiCodes } })
}
});
React.useEffect(() => {
useEffect(() => {
if (emoji.valid != shortcodeField.valid) {
onChange({ valid: shortcodeField.valid });
}
}, [onChange, emoji.valid, shortcodeField.valid]);
React.useEffect(() => {
useEffect(() => {
shortcodeField.validate();
// only need this update if it's the emoji.checked that updated, not shortcodeField
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -17,26 +17,23 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { useRoute, Redirect } = require("wouter");
import React, { useState } from "react";
import { useRoute, Redirect } from "wouter";
const FormWithData = require("../../lib/form/form-with-data").default;
const BackButton = require("../../components/back-button");
import FormWithData from "../../lib/form/form-with-data";
import BackButton from "../../components/back-button";
const { useValue, useTextInput } = require("../../lib/form");
const useFormSubmit = require("../../lib/form/submit").default;
import { useValue, useTextInput } from "../../lib/form";
import useFormSubmit from "../../lib/form/submit";
const { TextArea } = require("../../components/form/inputs");
import { TextArea } from "../../components/form/inputs";
const MutationButton = require("../../components/form/mutation-button");
const Username = require("./username");
const { useBaseUrl } = require("../../lib/navigation/util");
const {
useGetReportQuery,
useResolveReportMutation,
} = require("../../lib/query/admin/reports");
import MutationButton from "../../components/form/mutation-button";
import Username from "./username";
import { useBaseUrl } from "../../lib/navigation/util";
import { useGetReportQuery, useResolveReportMutation } from "../../lib/query/admin/reports";
module.exports = function ReportDetail({ }) {
export default function ReportDetail({ }) {
const baseUrl = useBaseUrl();
let [_match, params] = useRoute(`${baseUrl}/:reportId`);
if (params?.reportId == undefined) {
@@ -55,7 +52,7 @@ module.exports = function ReportDetail({ }) {
</div>
);
}
};
}
function ReportDetailForm({ data: report }) {
const from = report.account;
@@ -131,7 +128,11 @@ function ReportActionForm({ report }) {
field={form.comment}
label="Comment"
/>
<MutationButton label="Resolve" result={result} />
<MutationButton
disabled={false}
label="Resolve"
result={result}
/>
</form>
);
}
@@ -170,10 +171,10 @@ function ReportedToot({ toot }) {
}
</section>
<aside className="status-info">
<dl class="status-stats">
<div class="stats-grouping">
<div class="stats-item published-at text-cutoff">
<dt class="sr-only">Published</dt>
<dl className="status-stats">
<div className="stats-grouping">
<div className="stats-item published-at text-cutoff">
<dt className="sr-only">Published</dt>
<dd>
<time dateTime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time>
</dd>
@@ -186,7 +187,7 @@ function ReportedToot({ toot }) {
}
function TootCW({ note, content }) {
const [visible, setVisible] = React.useState(false);
const [visible, setVisible] = useState(false);
function toggleVisible() {
setVisible(!visible);
@@ -217,12 +218,12 @@ function TootMedia({ media, sensitive }) {
<input id={`sensitiveMedia-${m.id}`} type="checkbox" className="sensitive-checkbox hidden" />
<div className="sensitive">
<div className="open">
<label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex="0">
<label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex={0}>
<i className="fa fa-eye-slash" title="Hide sensitive media"></i>
</label>
</div>
<div className="closed" title={m.description}>
<label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex="0">
<label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex={0}>
Show sensitive media
</label>
</div>
@@ -241,12 +242,11 @@ function TootMedia({ media, sensitive }) {
alt={m.description}
src={m.url}
// thumb={m.preview_url}
size={m.meta?.original}
type={m.type}
sizes={m.meta?.original}
/>
</a>
</div>
))}
</div>
);
}
}

View File

@@ -17,17 +17,17 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { Link, Switch, Route } = require("wouter");
import React from "react";
import { Link, Switch, Route } from "wouter";
const FormWithData = require("../../lib/form/form-with-data").default;
import FormWithData from "../../lib/form/form-with-data";
const ReportDetail = require("./detail");
const Username = require("./username");
const { useBaseUrl } = require("../../lib/navigation/util");
const { useListReportsQuery } = require("../../lib/query/admin/reports");
import ReportDetail from "./detail";
import Username from "./username";
import { useBaseUrl } from "../../lib/navigation/util";
import { useListReportsQuery } from "../../lib/query/admin/reports";
module.exports = function Reports({ baseUrl }) {
export default function Reports({ baseUrl }) {
return (
<div className="reports">
<Switch>
@@ -38,7 +38,7 @@ module.exports = function Reports({ baseUrl }) {
</Switch>
</div>
);
};
}
function ReportOverview({ }) {
return (
@@ -100,4 +100,4 @@ function ReportEntry({ report }) {
</a>
</Link>
);
}
}

View File

@@ -17,10 +17,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { Link } = require("wouter");
import React from "react";
import { Link } from "wouter";
module.exports = function Username({ user, link = true }) {
export default function Username({ user, link = true }) {
let className = "user";
let isLocal = user.domain == null;
@@ -36,8 +36,8 @@ module.exports = function Username({ user, link = true }) {
? { fa: "fa-home", info: "Local user" }
: { fa: "fa-external-link-square", info: "Remote user" };
let Element = "div";
let href = null;
let Element: any = "div";
let href: any = null;
if (link) {
Element = Link;
@@ -51,4 +51,4 @@ module.exports = function Username({ user, link = true }) {
<span className="sr-only">{icon.info}</span>
</Element>
);
};
}

View File

@@ -17,28 +17,29 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { Switch, Route, Link, Redirect, useRoute } = require("wouter");
import React from "react";
import { Switch, Route, Link, Redirect, useRoute } from "wouter";
const query = require("../../lib/query");
const FormWithData = require("../../lib/form/form-with-data").default;
const { useBaseUrl } = require("../../lib/navigation/util");
import { useInstanceRulesQuery, useAddInstanceRuleMutation, useUpdateInstanceRuleMutation, useDeleteInstanceRuleMutation } from "../../lib/query";
import FormWithData from "../../lib/form/form-with-data";
import { useBaseUrl } from "../../lib/navigation/util";
const { useValue, useTextInput } = require("../../lib/form");
const useFormSubmit = require("../../lib/form/submit").default;
import { useValue, useTextInput } from "../../lib/form";
import useFormSubmit from "../../lib/form/submit";
const { TextArea } = require("../../components/form/inputs");
const MutationButton = require("../../components/form/mutation-button");
import { TextArea } from "../../components/form/inputs";
import MutationButton from "../../components/form/mutation-button";
import { Error } from "../../components/error";
module.exports = function InstanceRulesData({ baseUrl }) {
export default function InstanceRulesData({ baseUrl }) {
return (
<FormWithData
dataQuery={query.useInstanceRulesQuery}
dataQuery={useInstanceRulesQuery}
DataForm={InstanceRules}
baseUrl={baseUrl}
{...{baseUrl}}
/>
);
};
}
function InstanceRules({ baseUrl, data: rules }) {
return (
@@ -64,7 +65,8 @@ function InstanceRules({ baseUrl, data: rules }) {
function InstanceRuleList({ rules }) {
const newRule = useTextInput("text", {});
const [submitForm, result] = useFormSubmit({ newRule }, query.useAddInstanceRuleMutation(), {
const [submitForm, result] = useFormSubmit({ newRule }, useAddInstanceRuleMutation(), {
changedOnly: true,
onFinish: () => newRule.reset()
});
@@ -72,7 +74,7 @@ function InstanceRuleList({ rules }) {
<>
<form onSubmit={submitForm} className="new-rule">
<ol className="instance-rules">
{Object.values(rules).map((rule) => (
{Object.values(rules).map((rule: any) => (
<InstanceRule key={rule.id} rule={rule} />
))}
</ol>
@@ -80,7 +82,11 @@ function InstanceRuleList({ rules }) {
field={newRule}
label="New instance rule"
/>
<MutationButton label="Add rule" result={result} />
<MutationButton
disabled={newRule.value === undefined || newRule.value.length === 0}
label="Add rule"
result={result}
/>
</form>
</>
);
@@ -124,9 +130,9 @@ function InstanceRuleForm({ rule }) {
rule: useTextInput("text", { defaultValue: rule.text })
};
const [submitForm, result] = useFormSubmit(form, query.useUpdateInstanceRuleMutation());
const [submitForm, result] = useFormSubmit(form, useUpdateInstanceRuleMutation());
const [deleteRule, deleteResult] = query.useDeleteInstanceRuleMutation({ fixedCacheKey: rule.id });
const [deleteRule, deleteResult] = useDeleteInstanceRuleMutation({ fixedCacheKey: rule.id });
if (result.isSuccess || deleteResult.isSuccess) {
return (
@@ -150,6 +156,7 @@ function InstanceRuleForm({ rule }) {
/>
<MutationButton
disabled={false}
type="button"
onClick={() => deleteRule(rule.id)}
label="Delete"
@@ -164,4 +171,4 @@ function InstanceRuleForm({ rule }) {
</form>
</div>
);
}
}

View File

@@ -0,0 +1,82 @@
/*
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 { Link } from "wouter";
import { Error } from "./error";
import { AdminAccount } from "../lib/types/account";
import { SerializedError } from "@reduxjs/toolkit";
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
export interface AccountListProps {
isSuccess: boolean,
data: AdminAccount[] | undefined,
isLoading: boolean,
isError: boolean,
error: FetchBaseQueryError | SerializedError | undefined,
emptyMessage: string,
}
export function AccountList({
isLoading,
isSuccess,
data,
isError,
error,
emptyMessage,
}: AccountListProps) {
if (!(isSuccess || isError)) {
// Hasn't been called yet.
return null;
}
if (isLoading) {
return <i
className="fa fa-fw fa-refresh fa-spin"
aria-hidden="true"
title="Loading..."
/>;
}
if (error) {
return <Error error={error} />;
}
if (data == undefined || data.length == 0) {
return <b>{emptyMessage}</b>;
}
return (
<div className="list">
{data.map(({ account: acc }) => (
<Link
key={acc.acct}
className="account entry"
href={`/settings/admin/accounts/${acc.id}`}
>
{acc.display_name?.length > 0
? acc.display_name
: acc.username
}
<span id="username">(@{acc.acct})</span>
</Link>
))}
</div>
);
}

View File

@@ -1,48 +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/>.
*/
const React = require("react");
const { Error } = require("../error");
module.exports = function MutationButton({ label, result, disabled, showError = true, className = "", wrapperClassName = "", ...inputProps }) {
let iconClass = "";
const targetsThisButton = result.action == inputProps.name; // can also both be undefined, which is correct
if (targetsThisButton) {
if (result.isLoading) {
iconClass = "fa-spin fa-refresh";
} else if (result.isSuccess) {
iconClass = "fa-check fadeout";
}
}
return (<div className={wrapperClassName}>
{(showError && targetsThisButton && result.error) &&
<Error error={result.error} />
}
<button type="submit" className={"with-icon " + className} disabled={result.isLoading || disabled} {...inputProps}>
<i className={`fa fa-fw ${iconClass}`} aria-hidden="true"></i>
{(targetsThisButton && result.isLoading)
? "Processing..."
: label
}
</button>
</div>
);
};

View File

@@ -0,0 +1,72 @@
/*
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 { Error } from "../error";
export interface MutationButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
label: string,
result,
disabled: boolean,
showError?: boolean,
className?: string,
wrapperClassName?: string,
}
export default function MutationButton({
label,
result,
disabled,
showError = true,
className = "",
wrapperClassName = "",
...inputProps
}: MutationButtonProps) {
let iconClass = "";
// Can also both be undefined, which is correct.
const targetsThisButton = result.action == inputProps.name;
if (targetsThisButton) {
if (result.isLoading) {
iconClass = " fa-spin fa-refresh";
} else if (result.isSuccess) {
iconClass = " fa-check fadeout";
}
}
return (
<div className={wrapperClassName}>
{(showError && targetsThisButton && result.error) &&
<Error error={result.error} />
}
<button
type="submit"
className={"with-icon " + className}
disabled={result.isLoading || disabled}
{...inputProps}
>
<i className={`fa fa-fw${iconClass}`} aria-hidden="true"></i>
{(targetsThisButton && result.isLoading)
? "Processing..."
: label
}
</button>
</div>
);
}

View File

@@ -34,10 +34,22 @@ const UserProfile = require("./user/profile").default;
const UserSettings = require("./user/settings").default;
const UserMigration = require("./user/migration").default;
const Reports = require("./admin/reports").default;
const Accounts = require("./admin/accounts").default;
const AccountsPending = require("./admin/accounts/pending").default;
const DomainPerms = require("./admin/domain-permissions").default;
const DomainPermsImportExport = require("./admin/domain-permissions/import-export").default;
const AdminMedia = require("./admin/actions/media").default;
const AdminKeys = require("./admin/actions/keys").default;
const LocalEmoji = require("./admin/emoji/local").default;
const RemoteEmoji = require("./admin/emoji/remote").default;
const InstanceSettings = require("./admin/settings").default;
const InstanceRules = require("./admin/settings/rules").default;
require("./style.css");
@@ -51,8 +63,11 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
url: "admin",
permissions: ["admin"]
}, [
Item("Reports", { icon: "fa-flag", wildcard: true }, require("./admin/reports")),
Item("Accounts", { icon: "fa-users", wildcard: true }, require("./admin/accounts")),
Item("Reports", { icon: "fa-flag", wildcard: true }, Reports),
Item("Accounts", { icon: "fa-users", wildcard: true }, [
Item("Overview", { icon: "fa-list", url: "", wildcard: true }, Accounts),
Item("Pending", { icon: "fa-question", url: "pending", wildcard: true }, AccountsPending),
]),
Menu("Domain Permissions", { icon: "fa-hubzilla" }, [
Item("Blocks", { icon: "fa-close", url: "block", wildcard: true }, DomainPerms),
Item("Allows", { icon: "fa-check", url: "allow", wildcard: true }, DomainPerms),
@@ -65,16 +80,16 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
permissions: ["admin"]
}, [
Menu("Actions", { icon: "fa-bolt" }, [
Item("Media", { icon: "fa-photo" }, require("./admin/actions/media")),
Item("Keys", { icon: "fa-key-modern" }, require("./admin/actions/keys")),
Item("Media", { icon: "fa-photo" }, AdminMedia),
Item("Keys", { icon: "fa-key-modern" }, AdminKeys),
]),
Menu("Custom Emoji", { icon: "fa-smile-o" }, [
Item("Local", { icon: "fa-home", wildcard: true }, require("./admin/emoji/local")),
Item("Remote", { icon: "fa-cloud" }, require("./admin/emoji/remote"))
Item("Local", { icon: "fa-home", wildcard: true }, LocalEmoji),
Item("Remote", { icon: "fa-cloud" }, RemoteEmoji),
]),
Menu("Settings", { icon: "fa-sliders" }, [
Item("Settings", { icon: "fa-sliders", url: "" }, InstanceSettings),
Item("Rules", { icon: "fa-dot-circle-o", wildcard: true }, require("./admin/settings/rules"))
Item("Rules", { icon: "fa-dot-circle-o", wildcard: true }, InstanceRules),
]),
])
]);

View File

@@ -17,16 +17,16 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const RoleContext = React.createContext([]);
const BaseUrlContext = React.createContext(null);
import { createContext, useContext } from "react";
const RoleContext = createContext([]);
const BaseUrlContext = createContext<string>("");
function urlSafe(str) {
return str.toLowerCase().replace(/[\s/]+/g, "-");
}
function useHasPermission(permissions) {
const roles = React.useContext(RoleContext);
const roles = useContext(RoleContext);
return checkPermission(permissions, roles);
}
@@ -41,9 +41,14 @@ function checkPermission(requiredPermissisons, user) {
}
function useBaseUrl() {
return React.useContext(BaseUrlContext);
return useContext(BaseUrlContext);
}
module.exports = {
urlSafe, RoleContext, useHasPermission, checkPermission, BaseUrlContext, useBaseUrl
};
export {
urlSafe,
RoleContext,
useHasPermission,
checkPermission,
BaseUrlContext,
useBaseUrl
};

View File

@@ -20,6 +20,7 @@
import { replaceCacheOnMutation, removeFromCacheOnMutation } from "../query-modifiers";
import { gtsApi } from "../gts-api";
import { listToKeyedObject } from "../transforms";
import { AdminAccount, HandleSignupParams, SearchAccountParams } from "../../types/account";
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
@@ -54,14 +55,43 @@ const extended = gtsApi.injectEndpoints({
})
}),
getAccount: build.query({
getAccount: build.query<AdminAccount, string>({
query: (id) => ({
url: `/api/v1/accounts/${id}`
url: `/api/v1/admin/accounts/${id}`
}),
providesTags: (_, __, id) => [{ type: "Account", id }]
providesTags: (_result, _error, id) => [
{ type: 'Account', id }
],
}),
actionAccount: build.mutation({
searchAccounts: build.query<AdminAccount[], SearchAccountParams>({
query: (form) => {
const params = new(URLSearchParams);
Object.entries(form).forEach(([k, v]) => {
if (v !== undefined) {
params.append(k, v);
}
});
let query = "";
if (params.size !== 0) {
query = `?${params.toString()}`;
}
return {
url: `/api/v2/admin/accounts${query}`
};
},
providesTags: (res) =>
res
? [
...res.map(({ id }) => ({ type: 'Account' as const, id })),
{ type: 'Account', id: 'LIST' },
]
: [{ type: 'Account', id: 'LIST' }],
}),
actionAccount: build.mutation<string, { id: string, action: string, reason: string }>({
query: ({ id, action, reason }) => ({
method: "POST",
url: `/api/v1/admin/accounts/${id}/action`,
@@ -71,16 +101,23 @@ const extended = gtsApi.injectEndpoints({
text: reason
}
}),
invalidatesTags: (_, __, { id }) => [{ type: "Account", id }]
invalidatesTags: (_result, _error, { id }) => [
{ type: 'Account', id },
],
}),
searchAccount: build.mutation({
query: (username) => ({
url: `/api/v2/search?q=${encodeURIComponent(username)}&resolve=true`
}),
transformResponse: (res) => {
return res.accounts ?? [];
}
handleSignup: build.mutation<AdminAccount, HandleSignupParams>({
query: ({id, approve_or_reject, ...formData}) => {
return {
method: "POST",
url: `/api/v1/admin/accounts/${id}/${approve_or_reject}`,
asForm: true,
body: approve_or_reject === "reject" ?? formData,
};
},
invalidatesTags: (_result, _error, { id }) => [
{ type: 'Account', id },
],
}),
instanceRules: build.query({
@@ -140,7 +177,9 @@ export const {
useInstanceKeysExpireMutation,
useGetAccountQuery,
useActionAccountMutation,
useSearchAccountMutation,
useSearchAccountsQuery,
useLazySearchAccountsQuery,
useHandleSignupMutation,
useInstanceRulesQuery,
useAddInstanceRuleMutation,
useUpdateInstanceRuleMutation,

View File

@@ -36,7 +36,7 @@ const extended = gtsApi.injectEndpoints({
...params
}
}),
providesTags: ["Reports"]
providesTags: [{ type: "Reports", id: "LIST" }]
}),
getReport: build.query<AdminReport, string>({

View File

@@ -0,0 +1,88 @@
/*
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 { CustomEmoji } from "./custom-emoji";
export interface AdminAccount {
id: string,
username: string,
domain: string | null,
created_at: string,
email: string,
ip: string | null,
ips: [],
locale: string,
invite_request: string | null,
role: any,
confirmed: boolean,
approved: boolean,
disabled: boolean,
silenced: boolean,
suspended: boolean,
created_by_application_id: string,
account: Account,
}
export interface Account {
id: string,
username: string,
acct: string,
display_name: string,
locked: boolean,
discoverable: boolean,
bot: boolean,
created_at: string,
note: string,
url: string,
avatar: string,
avatar_static: string,
header: string,
header_static: string,
followers_count: number,
following_count: number,
statuses_count: number,
last_status_at: string,
emojis: CustomEmoji[],
fields: [],
enable_rss: boolean,
role: any,
}
export interface SearchAccountParams {
origin?: "local" | "remote",
status?: "active" | "pending" | "disabled" | "silenced" | "suspended",
permissions?: "staff",
username?: string,
display_name?: string,
by_domain?: string,
email?: string,
ip?: string,
max_id?: string,
since_id?: string,
min_id?: string,
limit?: number,
}
export interface HandleSignupParams {
id: string,
approve_or_reject: "approve" | "reject",
private_comment?: string,
message?: string,
send_email?: boolean,
}

View File

@@ -804,16 +804,12 @@ span.form-info {
.info {
color: $info-fg;
background: $info-bg;
padding: 0.5rem;
padding: 0.25rem;
border-radius: $br;
display: flex;
gap: 0.5rem;
align-items: center;
i {
margin-top: 0.1em;
}
a {
color: $info-link;
@@ -1145,7 +1141,7 @@ button.with-padding {
}
}
.account-search {
.accounts-view {
form {
margin-bottom: 1rem;
}
@@ -1175,9 +1171,42 @@ button.with-padding {
max-width: 60rem;
}
h4, h3, h2 {
margin-top: 0;
margin-bottom: 0;
}
.info-list {
border: 0.1rem solid $gray1;
display: flex;
flex-direction: column;
.info-list-entry {
background: $list-entry-bg;
border: 0.1rem solid transparent;
padding: 0.25rem;
&:nth-child(even) {
background: $list-entry-alternate-bg;
}
display: grid;
grid-template-columns: max(20%, 10rem) 1fr;
dt {
font-weight: bold;
}
dd {
word-break: break-word;
}
}
}
.action-buttons {
display: flex;
gap: 0.5rem;
align-items: center;
}
}

View File

@@ -27,4 +27,6 @@ To confirm your email, paste the following in your browser's address bar:
{{ .ConfirmLink }}
---
If you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of {{ .InstanceURL -}}.

View File

@@ -25,3 +25,7 @@ The report you submitted has now been closed.
{{ if .ActionTakenComment }}The moderator who closed the report left the following comment: {{ .ActionTakenComment }}
{{- else }}The moderator who closed the report did not leave a comment.{{ end }}
---
If you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of {{ .InstanceURL -}}.

View File

@@ -25,4 +25,6 @@ To reset your password, paste the following in your browser's address bar:
{{.ResetLink}}
---
If you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of {{.InstanceURL}}.

View File

@@ -0,0 +1,34 @@
{{- /*
// 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/>.
*/ -}}
Hello {{ .Username -}}!
You are receiving this mail because your request for an account on {{ .InstanceName }} has been approved by a moderator. Welcome!
If you have already confirmed your email address, you can now log in to your new account using a client application of your choice.
Some client applications known to work with GoToSocial are listed here: {{ .InstanceURL -}}#apps.
If you have not yet confirmed your email address, you will not be able to log in until you have done so.
Please check your inbox for the relevant email containing the confirmation link.
---
If you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of {{ .InstanceURL -}}.

View File

@@ -0,0 +1,28 @@
{{- /*
// 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/>.
*/ -}}
Hello!
You are receiving this mail because your request for an account on {{ .InstanceName }} has been rejected by a moderator.
{{ if .Message }}The moderator who handled the sign-up included the following message regarding this rejection: "{{- .Message -}}"{{ end }}
---
If you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of {{ .InstanceURL -}}.