mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 17:42:25 -05:00
[feature] Allow import/export/creation of domain allows via admin panel (#2264)
* it's happening! * aaa * fix silly whoopsie * it's working pa! it's working ma! * model report parameters * shuffle some more stuff around * getting there * oo hoo * finish tidying up for now * aaa * fix use form submit errors * peepee poo poo * aaaaa * ffff * they see me typin', they hatin' * boop * aaa * oooo * typing typing tappa tappa * almost done typing * weee * alright * push it push it real good doo doo doo doo doo doo * thingy no worky * almost done * mutation modifers not quite right * hmm * it works * view blocks + allows nicely * it works! * typia install * the old linterino * linter plz
This commit is contained in:
parent
48725f7228
commit
637f188ebe
77 changed files with 4154 additions and 1690 deletions
254
web/source/settings/admin/domain-permissions/detail.tsx
Normal file
254
web/source/settings/admin/domain-permissions/detail.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
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 { useMemo } from "react";
|
||||
import { useLocation } 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";
|
||||
|
||||
export interface DomainPermDetailProps {
|
||||
baseUrl: string;
|
||||
permType: PermType;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export default function DomainPermDetail({ baseUrl, permType, domain }: DomainPermDetailProps) {
|
||||
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";
|
||||
}
|
||||
|
||||
if (domain == "view") {
|
||||
// Retrieve domain from form field submission.
|
||||
domain = (new URL(document.location.toString())).searchParams.get("domain")?? "unknown";
|
||||
}
|
||||
|
||||
if (domain == "unknown") {
|
||||
throw "unknown domain";
|
||||
}
|
||||
|
||||
// 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 = <Loading />;
|
||||
} else if (existingPerm == undefined) {
|
||||
infoContent = <span>No stored {permType} yet, you can add one below:</span>;
|
||||
} else {
|
||||
infoContent = (
|
||||
<div className="info">
|
||||
<i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<b>Editing domain permissions isn't implemented yet, <a href="https://github.com/superseriousbusiness/gotosocial/issues/1198" target="_blank" rel="noopener noreferrer">check here for progress</a></b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-cutoff"><BackButton to={baseUrl} /> Domain {permType} for: <span title={domain}>{domain}</span></h1>
|
||||
{infoContent}
|
||||
<DomainPermForm
|
||||
defaultDomain={domain}
|
||||
perm={existingPerm}
|
||||
permType={permType}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DomainPermFormProps {
|
||||
defaultDomain: string;
|
||||
perm?: DomainPerm;
|
||||
permType: PermType;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
function DomainPermForm({ defaultDomain, perm, permType, baseUrl }: 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 = `${baseUrl}/${form.domain.value}`;
|
||||
if (location != correctUrl) {
|
||||
setLocation(correctUrl);
|
||||
}
|
||||
return submitForm(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={verifyUrlThenSubmit}>
|
||||
<TextInput
|
||||
field={form.domain}
|
||||
label="Domain"
|
||||
placeholder="example.com"
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
field={form.obfuscate}
|
||||
label="Obfuscate domain in public lists"
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.commentPrivate}
|
||||
label="Private comment"
|
||||
rows={3}
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.commentPublic}
|
||||
label="Public comment"
|
||||
rows={3}
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<div className="action-buttons row">
|
||||
<MutationButton
|
||||
label={permTypeUpper}
|
||||
result={submitFormResult}
|
||||
showError={false}
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
{
|
||||
isExistingPerm &&
|
||||
<MutationButton
|
||||
type="button"
|
||||
onClick={() => removeTrigger(perm.id?? "")}
|
||||
label="Remove"
|
||||
result={removeResult}
|
||||
className="button danger"
|
||||
showError={false}
|
||||
disabled={!isExistingPerm}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<>
|
||||
{addResult.error && <Error error={addResult.error} />}
|
||||
{removeResult.error && <Error error={removeResult.error} />}
|
||||
</>
|
||||
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
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");
|
||||
|
||||
module.exports = function ExportFormatTable() {
|
||||
return (
|
||||
<div className="export-format-table-wrapper without-border">
|
||||
<table className="export-format-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowSpan={2} />
|
||||
<th colSpan={2}>Includes</th>
|
||||
<th colSpan={2}>Importable by</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Public comment</th>
|
||||
<th>GoToSocial</th>
|
||||
<th>Mastodon</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Format name="Text" info={[true, false, true, false]} />
|
||||
<Format name="JSON" info={[true, true, true, false]} />
|
||||
<Format name="CSV" info={[true, true, true, true]} />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function Format({ name, info }) {
|
||||
return (
|
||||
<tr>
|
||||
<td><b>{name}</b></td>
|
||||
{info.map((b, key) => <td key={key} className="bool">{bool(b)}</td>)}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function bool(val) {
|
||||
return (
|
||||
<>
|
||||
<i className={`fa fa-${val ? "check" : "times"}`} aria-hidden="true"></i>
|
||||
<span className="sr-only">{val ? "Yes" : "No"}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
152
web/source/settings/admin/domain-permissions/form.tsx
Normal file
152
web/source/settings/admin/domain-permissions/form.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
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 { useEffect } from "react";
|
||||
|
||||
import { useExportDomainListMutation } from "../../lib/query/admin/domain-permissions/export";
|
||||
import useFormSubmit from "../../lib/form/submit";
|
||||
|
||||
import {
|
||||
RadioGroup,
|
||||
TextArea,
|
||||
Select,
|
||||
} from "../../components/form/inputs";
|
||||
|
||||
import MutationButton from "../../components/form/mutation-button";
|
||||
|
||||
import { Error } from "../../components/error";
|
||||
import ExportFormatTable from "./export-format-table";
|
||||
|
||||
import type {
|
||||
FormSubmitFunction,
|
||||
FormSubmitResult,
|
||||
RadioFormInputHook,
|
||||
TextFormInputHook,
|
||||
} from "../../lib/form/types";
|
||||
|
||||
export interface ImportExportFormProps {
|
||||
form: {
|
||||
domains: TextFormInputHook;
|
||||
exportType: TextFormInputHook;
|
||||
permType: RadioFormInputHook;
|
||||
};
|
||||
submitParse: FormSubmitFunction;
|
||||
parseResult: FormSubmitResult;
|
||||
}
|
||||
|
||||
export default function ImportExportForm({ form, submitParse, parseResult }: ImportExportFormProps) {
|
||||
const [submitExport, exportResult] = useFormSubmit(form, useExportDomainListMutation());
|
||||
|
||||
function fileChanged(e) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (read) {
|
||||
const res = read.target?.result;
|
||||
if (typeof res === "string") {
|
||||
form.domains.value = res;
|
||||
submitParse();
|
||||
}
|
||||
};
|
||||
reader.readAsText(e.target.files[0]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (exportResult.isSuccess) {
|
||||
form.domains.setter(exportResult.data);
|
||||
}
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [exportResult]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Import / Export domain permissions</h1>
|
||||
<p>This page can be used to import and export lists of domain permissions.</p>
|
||||
<p>Exports can be done in various formats, with varying functionality and support in other software.</p>
|
||||
<p>Imports will automatically detect what format is being processed.</p>
|
||||
<ExportFormatTable />
|
||||
<div className="import-export">
|
||||
<TextArea
|
||||
field={form.domains}
|
||||
label="Domains"
|
||||
placeholder={`google.com\nfacebook.com`}
|
||||
rows={8}
|
||||
/>
|
||||
|
||||
<RadioGroup
|
||||
field={form.permType}
|
||||
/>
|
||||
|
||||
<div className="button-grid">
|
||||
<MutationButton
|
||||
label="Import"
|
||||
type="button"
|
||||
onClick={() => submitParse()}
|
||||
result={parseResult}
|
||||
showError={false}
|
||||
disabled={false}
|
||||
/>
|
||||
<label className="button with-icon">
|
||||
<i className="fa fa-fw " aria-hidden="true" />
|
||||
Import file
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={fileChanged}
|
||||
accept="application/json,text/plain,text/csv"
|
||||
/>
|
||||
</label>
|
||||
<b /> {/* grid filler */}
|
||||
<MutationButton
|
||||
label="Export"
|
||||
type="button"
|
||||
onClick={() => submitExport("export")}
|
||||
result={exportResult} showError={false}
|
||||
disabled={false}
|
||||
/>
|
||||
<MutationButton
|
||||
label="Export to file"
|
||||
wrapperClassName="export-file-button"
|
||||
type="button"
|
||||
onClick={() => submitExport("export-file")}
|
||||
result={exportResult}
|
||||
showError={false}
|
||||
disabled={false}
|
||||
/>
|
||||
<div className="export-file">
|
||||
<span>
|
||||
as
|
||||
</span>
|
||||
<Select
|
||||
field={form.exportType}
|
||||
options={<>
|
||||
<option value="plain">Text</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="csv">CSV</option>
|
||||
</>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{parseResult.error && <Error error={parseResult.error} />}
|
||||
{exportResult.error && <Error error={exportResult.error} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
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, Redirect, useLocation } from "wouter";
|
||||
|
||||
import { useProcessDomainPermissionsMutation } from "../../lib/query/admin/domain-permissions/process";
|
||||
|
||||
import { useTextInput, useRadioInput } from "../../lib/form";
|
||||
|
||||
import useFormSubmit from "../../lib/form/submit";
|
||||
|
||||
import { ProcessImport } from "./process";
|
||||
import ImportExportForm from "./form";
|
||||
|
||||
export default function ImportExport({ baseUrl }) {
|
||||
const form = {
|
||||
domains: useTextInput("domains"),
|
||||
exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true }),
|
||||
permType: useRadioInput("permType", {
|
||||
options: {
|
||||
block: "Domain blocks",
|
||||
allow: "Domain allows",
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const [submitParse, parseResult] = useFormSubmit(form, useProcessDomainPermissionsMutation(), { changedOnly: false });
|
||||
|
||||
const [_location, setLocation] = useLocation();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${baseUrl}/process`}>
|
||||
{
|
||||
parseResult.isSuccess
|
||||
? (
|
||||
<>
|
||||
<h1>
|
||||
<span
|
||||
className="button"
|
||||
onClick={() => {
|
||||
parseResult.reset();
|
||||
setLocation(baseUrl);
|
||||
}}
|
||||
>
|
||||
< back
|
||||
</span>
|
||||
Confirm import of domain {form.permType.value}s:
|
||||
</h1>
|
||||
<ProcessImport
|
||||
list={parseResult.data}
|
||||
permType={form.permType}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: <Redirect to={baseUrl} />
|
||||
}
|
||||
</Route>
|
||||
<Route>
|
||||
{
|
||||
parseResult.isSuccess
|
||||
? <Redirect to={`${baseUrl}/process`} />
|
||||
: <ImportExportForm
|
||||
form={form}
|
||||
submitParse={submitParse}
|
||||
parseResult={parseResult}
|
||||
/>
|
||||
}
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
49
web/source/settings/admin/domain-permissions/index.tsx
Normal file
49
web/source/settings/admin/domain-permissions/index.tsx
Normal 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 DomainPermissionsOverview from "./overview";
|
||||
import { PermType } from "../../lib/types/domain-permission";
|
||||
import DomainPermDetail from "./detail";
|
||||
|
||||
export default function DomainPermissions({ baseUrl }: { baseUrl: string }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/settings/admin/domain-permissions/:permType/:domain">
|
||||
{params => (
|
||||
<DomainPermDetail
|
||||
permType={params.permType as PermType}
|
||||
baseUrl={baseUrl}
|
||||
domain={params.domain}
|
||||
/>
|
||||
)}
|
||||
</Route>
|
||||
<Route path="/settings/admin/domain-permissions/:permType">
|
||||
{params => (
|
||||
<DomainPermissionsOverview
|
||||
permType={params.permType as PermType}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
)}
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
198
web/source/settings/admin/domain-permissions/overview.tsx
Normal file
198
web/source/settings/admin/domain-permissions/overview.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
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 { useMemo } from "react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { matchSorter } from "match-sorter";
|
||||
|
||||
import { useTextInput } from "../../lib/form";
|
||||
|
||||
import { TextInput } from "../../components/form/inputs";
|
||||
|
||||
import Loading from "../../components/loading";
|
||||
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get";
|
||||
import type { MappedDomainPerms, PermType } from "../../lib/types/domain-permission";
|
||||
import { NoArg } from "../../lib/types/query";
|
||||
|
||||
export interface DomainPermissionsOverviewProps {
|
||||
// Params injected by
|
||||
// the wouter router.
|
||||
permType: PermType;
|
||||
baseUrl: string,
|
||||
}
|
||||
|
||||
export default function DomainPermissionsOverview({ permType, baseUrl }: DomainPermissionsOverviewProps) {
|
||||
if (permType !== "block" && permType !== "allow") {
|
||||
throw "unrecognized perm type " + permType;
|
||||
}
|
||||
|
||||
// Uppercase first letter of given permType.
|
||||
const permTypeUpper = useMemo(() => {
|
||||
return permType.charAt(0).toUpperCase() + permType.slice(1);
|
||||
}, [permType]);
|
||||
|
||||
// Fetch / wait for desired perms to load.
|
||||
const { data: blocks, isLoading: isLoadingBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" });
|
||||
const { data: allows, isLoading: isLoadingAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" });
|
||||
|
||||
let data: MappedDomainPerms | undefined;
|
||||
let isLoading: boolean;
|
||||
|
||||
if (permType == "block") {
|
||||
data = blocks;
|
||||
isLoading = isLoadingBlocks;
|
||||
} else {
|
||||
data = allows;
|
||||
isLoading = isLoadingAllows;
|
||||
}
|
||||
|
||||
if (isLoading || data === undefined) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Domain {permTypeUpper}s</h1>
|
||||
{ permType == "block" ? <BlockHelperText/> : <AllowHelperText/> }
|
||||
<DomainPermsList
|
||||
data={data}
|
||||
baseUrl={baseUrl}
|
||||
permType={permType}
|
||||
permTypeUpper={permTypeUpper}
|
||||
/>
|
||||
<Link to={`${baseUrl}/import-export`}>
|
||||
<a>Or use the bulk import/export interface</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DomainPermsListProps {
|
||||
data: MappedDomainPerms;
|
||||
baseUrl: string;
|
||||
permType: PermType;
|
||||
permTypeUpper: string;
|
||||
}
|
||||
|
||||
function DomainPermsList({ data, baseUrl, permType, permTypeUpper }: DomainPermsListProps) {
|
||||
// Format perms into a list.
|
||||
const perms = useMemo(() => {
|
||||
return Object.values(data);
|
||||
}, [data]);
|
||||
|
||||
const [_location, setLocation] = useLocation();
|
||||
const filterField = useTextInput("filter");
|
||||
|
||||
function filterFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
setLocation(`${baseUrl}/${filter}`);
|
||||
}
|
||||
|
||||
const filter = filterField.value ?? "";
|
||||
const filteredPerms = useMemo(() => {
|
||||
return matchSorter(perms, filter, { keys: ["domain"] });
|
||||
}, [perms, filter]);
|
||||
const filtered = perms.length - filteredPerms.length;
|
||||
|
||||
const filterInfo = (
|
||||
<span>
|
||||
{perms.length} {permType}ed domain{perms.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered by search)`}
|
||||
</span>
|
||||
);
|
||||
|
||||
const entries = filteredPerms.map((entry) => {
|
||||
return (
|
||||
<Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}>
|
||||
<a className="entry nounderline">
|
||||
<span id="domain">{entry.domain}</span>
|
||||
<span id="date">{new Date(entry.created_at ?? "").toLocaleString()}</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="domain-permissions-list">
|
||||
<form className="filter" role="search" onSubmit={filterFormSubmit}>
|
||||
<TextInput
|
||||
field={filterField}
|
||||
placeholder="example.org"
|
||||
label={`Search or add domain ${permType}`}
|
||||
/>
|
||||
<Link to={`${baseUrl}/${filter}`}>
|
||||
<a className="button">{permTypeUpper} {filter}</a>
|
||||
</Link>
|
||||
</form>
|
||||
<div>
|
||||
{filterInfo}
|
||||
<div className="list">
|
||||
<div className="entries scrolling">
|
||||
{entries}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BlockHelperText() {
|
||||
return (
|
||||
<p>
|
||||
Blocking a domain blocks interaction between your instance, and all current and future accounts on
|
||||
instance(s) running on the blocked domain. Stored content will be removed, and no more data is sent to
|
||||
the remote server. This extends to all subdomains as well, so blocking 'example.com' also blocks 'social.example.com'.
|
||||
<br/>
|
||||
<a
|
||||
href="https://docs.gotosocial.org/en/latest/admin/domain_blocks/"
|
||||
target="_blank"
|
||||
className="docslink"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more about domain blocks (opens in a new tab)
|
||||
</a>
|
||||
<br/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function AllowHelperText() {
|
||||
return (
|
||||
<p>
|
||||
Allowing a domain explicitly allows instance(s) running on that domain to interact with your instance.
|
||||
If you're running in allowlist mode, this is how you "allow" instances through.
|
||||
If you're running in blocklist mode (the default federation mode), you can use explicit domain allows
|
||||
to override domain blocks. In blocklist mode, explicitly allowed instances will be able to interact with
|
||||
your instance regardless of any domain blocks in place. This extends to all subdomains as well, so allowing
|
||||
'example.com' also allows 'social.example.com'. This is useful when you're importing a block list but
|
||||
there are some domains on the list you don't want to block: just create an explicit allow for those domains
|
||||
before importing the list.
|
||||
<br/>
|
||||
<a
|
||||
href="https://docs.gotosocial.org/en/latest/admin/federation_modes/"
|
||||
target="_blank"
|
||||
className="docslink"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more about federation modes (opens in a new tab)
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
402
web/source/settings/admin/domain-permissions/process.tsx
Normal file
402
web/source/settings/admin/domain-permissions/process.tsx
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
/*
|
||||
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 { memo, useMemo, useCallback, useEffect } from "react";
|
||||
|
||||
import { isValidDomainPermission, hasBetterScope } from "../../lib/util/domain-permission";
|
||||
|
||||
import {
|
||||
useTextInput,
|
||||
useBoolInput,
|
||||
useRadioInput,
|
||||
useCheckListInput,
|
||||
} from "../../lib/form";
|
||||
|
||||
import {
|
||||
Select,
|
||||
TextArea,
|
||||
RadioGroup,
|
||||
Checkbox,
|
||||
TextInput,
|
||||
} from "../../components/form/inputs";
|
||||
|
||||
import useFormSubmit from "../../lib/form/submit";
|
||||
|
||||
import CheckList from "../../components/check-list";
|
||||
import MutationButton from "../../components/form/mutation-button";
|
||||
import FormWithData from "../../lib/form/form-with-data";
|
||||
|
||||
import { useImportDomainPermsMutation } from "../../lib/query/admin/domain-permissions/import";
|
||||
import {
|
||||
useDomainAllowsQuery,
|
||||
useDomainBlocksQuery
|
||||
} from "../../lib/query/admin/domain-permissions/get";
|
||||
|
||||
import type { DomainPerm, MappedDomainPerms } from "../../lib/types/domain-permission";
|
||||
import type { ChecklistInputHook, RadioFormInputHook } from "../../lib/form/types";
|
||||
|
||||
export interface ProcessImportProps {
|
||||
list: DomainPerm[],
|
||||
permType: RadioFormInputHook,
|
||||
}
|
||||
|
||||
export const ProcessImport = memo(
|
||||
function ProcessImport({ list, permType }: ProcessImportProps) {
|
||||
return (
|
||||
<div className="without-border">
|
||||
<FormWithData
|
||||
dataQuery={permType.value == "allow"
|
||||
? useDomainAllowsQuery
|
||||
: useDomainBlocksQuery
|
||||
}
|
||||
DataForm={ImportList}
|
||||
{...{ list, permType }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export interface ImportListProps {
|
||||
list: Array<DomainPerm>,
|
||||
data: MappedDomainPerms,
|
||||
permType: RadioFormInputHook,
|
||||
}
|
||||
|
||||
function ImportList({ list, data: domainPerms, permType }: ImportListProps) {
|
||||
const hasComment = useMemo(() => {
|
||||
let hasPublic = false;
|
||||
let hasPrivate = false;
|
||||
|
||||
list.some((entry) => {
|
||||
if (entry.public_comment) {
|
||||
hasPublic = true;
|
||||
}
|
||||
|
||||
if (entry.private_comment) {
|
||||
hasPrivate = true;
|
||||
}
|
||||
|
||||
return hasPublic && hasPrivate;
|
||||
});
|
||||
|
||||
if (hasPublic && hasPrivate) {
|
||||
return { both: true };
|
||||
} else if (hasPublic) {
|
||||
return { type: "public_comment" };
|
||||
} else if (hasPrivate) {
|
||||
return { type: "private_comment" };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}, [list]);
|
||||
|
||||
const showComment = useTextInput("showComment", { defaultValue: hasComment.type ?? "public_comment" });
|
||||
|
||||
const form = {
|
||||
domains: useCheckListInput("domains", { entries: list }), // DomainPerm is actually also a Checkable.
|
||||
obfuscate: useBoolInput("obfuscate"),
|
||||
privateComment: useTextInput("private_comment", {
|
||||
defaultValue: `Imported on ${new Date().toLocaleString()}`
|
||||
}),
|
||||
privateCommentBehavior: useRadioInput("private_comment_behavior", {
|
||||
defaultValue: "append",
|
||||
options: {
|
||||
append: "Append to",
|
||||
replace: "Replace"
|
||||
}
|
||||
}),
|
||||
publicComment: useTextInput("public_comment"),
|
||||
publicCommentBehavior: useRadioInput("public_comment_behavior", {
|
||||
defaultValue: "append",
|
||||
options: {
|
||||
append: "Append to",
|
||||
replace: "Replace"
|
||||
}
|
||||
}),
|
||||
permType: permType,
|
||||
};
|
||||
|
||||
const [importDomains, importResult] = useFormSubmit(form, useImportDomainPermsMutation(), { changedOnly: false });
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={importDomains}
|
||||
className="domain-perm-import-list"
|
||||
>
|
||||
<span>{list.length} domain{list.length != 1 ? "s" : ""} in this list</span>
|
||||
|
||||
{hasComment.both &&
|
||||
<Select field={showComment} options={
|
||||
<>
|
||||
<option value="public_comment">Show public comments</option>
|
||||
<option value="private_comment">Show private comments</option>
|
||||
</>
|
||||
} />
|
||||
}
|
||||
|
||||
<div className="checkbox-list-wrapper">
|
||||
<DomainCheckList
|
||||
field={form.domains}
|
||||
domainPerms={domainPerms}
|
||||
commentType={showComment.value as "public_comment" | "private_comment"}
|
||||
permType={form.permType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
field={form.privateComment}
|
||||
label="Private comment"
|
||||
rows={3}
|
||||
/>
|
||||
<RadioGroup
|
||||
field={form.privateCommentBehavior}
|
||||
label="imported private comment"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
field={form.publicComment}
|
||||
label="Public comment"
|
||||
rows={3}
|
||||
/>
|
||||
<RadioGroup
|
||||
field={form.publicCommentBehavior}
|
||||
label="imported public comment"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
field={form.obfuscate}
|
||||
label="Obfuscate domains in public lists"
|
||||
/>
|
||||
|
||||
<MutationButton
|
||||
label="Import"
|
||||
disabled={false}
|
||||
result={importResult}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface DomainCheckListProps {
|
||||
field: ChecklistInputHook,
|
||||
domainPerms: MappedDomainPerms,
|
||||
commentType: "public_comment" | "private_comment",
|
||||
permType: RadioFormInputHook,
|
||||
}
|
||||
|
||||
function DomainCheckList({ field, domainPerms, commentType, permType }: DomainCheckListProps) {
|
||||
const getExtraProps = useCallback((entry: DomainPerm) => {
|
||||
return {
|
||||
comment: entry[commentType],
|
||||
alreadyExists: entry.domain in domainPerms,
|
||||
permType: permType,
|
||||
};
|
||||
}, [domainPerms, commentType, permType]);
|
||||
|
||||
const entriesWithSuggestions = useMemo(() => {
|
||||
const fieldValue = (field.value ?? {}) as { [k: string]: DomainPerm; };
|
||||
return Object.values(fieldValue).filter((entry) => entry.suggest);
|
||||
}, [field.value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CheckList
|
||||
field={field as ChecklistInputHook}
|
||||
header={<>
|
||||
<b>Domain</b>
|
||||
<b>
|
||||
{commentType == "public_comment" && "Public comment"}
|
||||
{commentType == "private_comment" && "Private comment"}
|
||||
</b>
|
||||
</>}
|
||||
EntryComponent={DomainEntry}
|
||||
getExtraProps={getExtraProps}
|
||||
/>
|
||||
<UpdateHint
|
||||
entries={entriesWithSuggestions}
|
||||
updateEntry={field.onChange}
|
||||
updateMultiple={field.updateMultiple}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface UpdateHintProps {
|
||||
entries,
|
||||
updateEntry,
|
||||
updateMultiple,
|
||||
}
|
||||
|
||||
const UpdateHint = memo(
|
||||
function UpdateHint({ entries, updateEntry, updateMultiple }: UpdateHintProps) {
|
||||
if (entries.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function changeAll() {
|
||||
updateMultiple(
|
||||
entries.map((entry) => [entry.key, { domain: entry.suggest, suggest: null }])
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="update-hints">
|
||||
<p>
|
||||
{entries.length} {entries.length == 1 ? "entry uses" : "entries use"} a specific subdomain,
|
||||
which you might want to change to the main domain, as that includes all it's (future) subdomains.
|
||||
</p>
|
||||
<div className="hints">
|
||||
{entries.map((entry) => (
|
||||
<UpdateableEntry key={entry.key} entry={entry} updateEntry={updateEntry} />
|
||||
))}
|
||||
</div>
|
||||
{entries.length > 0 && <a onClick={changeAll}>change all</a>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface UpdateableEntryProps {
|
||||
entry,
|
||||
updateEntry,
|
||||
}
|
||||
|
||||
const UpdateableEntry = memo(
|
||||
function UpdateableEntry({ entry, updateEntry }: UpdateableEntryProps) {
|
||||
return (
|
||||
<>
|
||||
<span className="text-cutoff">{entry.domain}</span>
|
||||
<i className="fa fa-long-arrow-right" aria-hidden="true"></i>
|
||||
<span>{entry.suggest}</span>
|
||||
<a role="button" onClick={() =>
|
||||
updateEntry(entry.key, { domain: entry.suggest, suggest: null })
|
||||
}>change</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function domainValidationError(isValid) {
|
||||
return isValid ? "" : "Invalid domain";
|
||||
}
|
||||
|
||||
interface DomainEntryProps {
|
||||
entry;
|
||||
onChange;
|
||||
extraProps: {
|
||||
alreadyExists: boolean;
|
||||
comment: string;
|
||||
permType: RadioFormInputHook;
|
||||
};
|
||||
}
|
||||
|
||||
function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment, permType } }: DomainEntryProps) {
|
||||
const domainField = useTextInput("domain", {
|
||||
defaultValue: entry.domain,
|
||||
showValidation: entry.checked,
|
||||
initValidation: domainValidationError(entry.valid),
|
||||
validator: (value) => domainValidationError(isValidDomainPermission(value))
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (entry.valid != domainField.valid) {
|
||||
onChange({ valid: domainField.valid });
|
||||
}
|
||||
}, [onChange, entry.valid, domainField.valid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (entry.domain != domainField.value) {
|
||||
domainField.setter(entry.domain);
|
||||
}
|
||||
// domainField.setter is enough, eslint wants domainField
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [entry.domain, domainField.setter]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({ suggest: hasBetterScope(domainField.value ?? "") });
|
||||
// only need this update if it's the entry.checked that updated, not onChange
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [domainField.value]);
|
||||
|
||||
function clickIcon(e) {
|
||||
if (entry.suggest) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
domainField.setter(entry.suggest);
|
||||
onChange({ domain: entry.suggest, checked: true });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="domain-input">
|
||||
<TextInput
|
||||
field={domainField}
|
||||
onChange={(e) => {
|
||||
domainField.onChange(e);
|
||||
onChange({ domain: e.target.value, checked: true });
|
||||
}}
|
||||
/>
|
||||
<span id="icon" onClick={clickIcon}>
|
||||
<DomainEntryIcon
|
||||
alreadyExists={alreadyExists}
|
||||
suggestion={entry.suggest}
|
||||
permTypeString={permType.value?? ""}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<p>{comment}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface DomainEntryIconProps {
|
||||
alreadyExists: boolean;
|
||||
suggestion: string;
|
||||
permTypeString: string;
|
||||
}
|
||||
|
||||
function DomainEntryIcon({ alreadyExists, suggestion, permTypeString }: DomainEntryIconProps) {
|
||||
let icon;
|
||||
let text;
|
||||
|
||||
if (suggestion) {
|
||||
icon = "fa-info-circle suggest-changes";
|
||||
text = `Entry targets a specific subdomain, consider changing it to '${suggestion}'.`;
|
||||
} else if (alreadyExists) {
|
||||
icon = "fa-history permission-already-exists";
|
||||
text = `Domain ${permTypeString} already exists.`;
|
||||
}
|
||||
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<i className={`fa fa-fw ${icon}`} aria-hidden="true" title={text}></i>
|
||||
<span className="sr-only">{text}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue