diff options
| author | 2024-05-05 13:47:22 +0200 | |
|---|---|---|
| committer | 2024-05-05 11:47:22 +0000 | |
| commit | 6171dcbe5109d7accbf44f19c20c9f4a0ee5e06f (patch) | |
| tree | 9011f0050571f5a8c1c0e7bd90b74b2816dadd8a /web/source/settings/views/admin/http-header-permissions | |
| parent | [frontend] Do optimistic update when approving/rejecting/suspending account (... (diff) | |
| download | gotosocial-6171dcbe5109d7accbf44f19c20c9f4a0ee5e06f.tar.xz | |
[feature] Add HTTP header permission section to frontend (#2893)
* [feature] Add HTTP header filter section to frontend
* tweak naming a bit
Diffstat (limited to 'web/source/settings/views/admin/http-header-permissions')
3 files changed, 558 insertions, 0 deletions
| diff --git a/web/source/settings/views/admin/http-header-permissions/create.tsx b/web/source/settings/views/admin/http-header-permissions/create.tsx new file mode 100644 index 000000000..b791ae0a9 --- /dev/null +++ b/web/source/settings/views/admin/http-header-permissions/create.tsx @@ -0,0 +1,143 @@ +/* +	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 { usePostHeaderAllowMutation, usePostHeaderBlockMutation } from "../../../lib/query/admin/http-header-permissions"; +import { useTextInput } from "../../../lib/form"; +import useFormSubmit from "../../../lib/form/submit"; +import { TextInput } from "../../../components/form/inputs"; +import MutationButton from "../../../components/form/mutation-button"; +import { PermType } from "../../../lib/types/perm"; + +export default function HeaderPermCreateForm({ permType }: { permType: PermType }) { +	const form = { +		header: useTextInput("header", { +			validator: (val: string) => { +				// Technically invalid but avoid +				// showing red outline when user +				// hasn't entered anything yet. +				if (val.length === 0) { +					return ""; +				} + +				// Only requirement is that header +				// must be less than 1024 chars. +				if (val.length > 1024) { +					return "header must be less than 1024 characters"; +				} + +				return ""; +			} +		}), +		regex: useTextInput("regex", { +			validator: (val: string) => { +				// Technically invalid but avoid +				// showing red outline when user +				// hasn't entered anything yet. +				if (val.length === 0) { +					return ""; +				} + +				// Ensure regex compiles. +				try { +					new RegExp(val); +				} catch (e) { +					return e; +				} + +				return ""; +			} +		}), +	}; + +	// Use appropriate mutation for given permType. +	const [ postAllowTrigger, postAllowResult ] = usePostHeaderAllowMutation(); +	const [ postBlockTrigger, postBlockResult ] = usePostHeaderBlockMutation(); + +	let mutationTrigger; +	let mutationResult; + +	if (permType === "block") { +		mutationTrigger = postBlockTrigger; +		mutationResult = postBlockResult; +	} else { +		mutationTrigger = postAllowTrigger; +		mutationResult = postAllowResult; +	} + +	const [formSubmit, result] = useFormSubmit( +		form, +		[mutationTrigger, mutationResult], +		{ +			changedOnly: false, +			onFinish: ({ _data }) => { +				form.header.reset(); +				form.regex.reset(); +			}, +		}); + +	return ( +		<form onSubmit={formSubmit}> +			<h2>Create new HTTP header {permType}</h2> +			<TextInput +				field={form.header} +				label={ +					<> +						HTTP Header Name  +						 <a +							href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" +							target="_blank" +							className="docslink" +							rel="noreferrer" +						> +							Learn more about HTTP request headers (opens in a new tab) +						</a> +					</> +				} +				placeholder={"User-Agent"} +			/> +			<TextInput +				field={form.regex} +				label={ +					<> +						HTTP Header Value Regex  +						<a +							href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions" +							target="_blank" +							className="docslink" +							rel="noreferrer" +						> +							Learn more about regular expressions (opens in a new tab) +						</a> +					</> +				} +				placeholder={"^.*Some-User-Agent.*$"} +				{...{className: "monospace"}} +			/> +			<MutationButton +				label="Save" +				result={result} +				disabled={ +					(!form.header.value || !form.regex.value) || +					(!form.header.valid || !form.regex.valid) +				} +			/> +		</form> +	); +} diff --git a/web/source/settings/views/admin/http-header-permissions/detail.tsx b/web/source/settings/views/admin/http-header-permissions/detail.tsx new file mode 100644 index 000000000..db92dd0eb --- /dev/null +++ b/web/source/settings/views/admin/http-header-permissions/detail.tsx @@ -0,0 +1,246 @@ +/* +	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, { useEffect, useMemo } from "react"; +import { useLocation, useParams } from "wouter"; +import { PermType } from "../../../lib/types/perm"; +import { useDeleteHeaderAllowMutation, useDeleteHeaderBlockMutation, useGetHeaderAllowQuery, useGetHeaderBlockQuery } from "../../../lib/query/admin/http-header-permissions"; +import { HeaderPermission } from "../../../lib/types/http-header-permissions"; +import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; +import { SerializedError } from "@reduxjs/toolkit"; +import Loading from "../../../components/loading"; +import { Error } from "../../../components/error"; +import { useLazyGetAccountQuery } from "../../../lib/query/admin"; +import Username from "../../../components/username"; +import { useBaseUrl } from "../../../lib/navigation/util"; +import BackButton from "../../../components/back-button"; +import MutationButton from "../../../components/form/mutation-button"; + +const testString = `/* To test this properly, set "flavor" to "Golang", as that's the language GoToSocial uses for regular expressions */ + +/* Amazon crawler User-Agent example */ +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/600.2.5 (KHTML\\, like Gecko) Version/8.0.2 Safari/600.2.5 (Amazonbot/0.1; +https://developer.amazon.com/support/amazonbot) + +/* Some other test strings */ +Some Test Value +Another Test Value`; + +export default function HeaderPermDetail() { +	let params = useParams(); +	if (params.permType !== "blocks" && params.permType !== "allows") { +		throw "unrecognized perm type " + params.permType; +	} +	const permType = useMemo(() => { +		return params.permType?.slice(0, -1) as PermType; +	}, [params]); + +	let permID = params.permId as string | undefined; +	if (!permID) { +		throw "no perm ID"; +	} + +	if (permType === "block") { +		return <BlockDetail id={permID} />; +	} else { +		return <AllowDetail id={permID} />; +	} +} + +function BlockDetail({ id }: { id: string }) { +	return ( +		<PermDeets +			permType={"Block"} +			{...useGetHeaderBlockQuery(id)} +		/> +	); +} + +function AllowDetail({ id }: { id: string }) { +	return ( +		<PermDeets +			permType={"Allow"} +			{...useGetHeaderAllowQuery(id)} +		/> +	); +} + +interface PermDeetsProps { +	permType: string; +	data?: HeaderPermission; +	isLoading: boolean; +	isFetching: boolean; +	isError: boolean; +	error?: FetchBaseQueryError | SerializedError; +} + +function PermDeets({ +	permType, +	data: perm, +	isLoading: isLoadingPerm, +	isFetching: isFetchingPerm, +	isError: isErrorPerm, +	error: errorPerm, +}: PermDeetsProps) { +	const [ location ] = useLocation(); +	const baseUrl = useBaseUrl(); +	 +	// Once we've loaded the perm, trigger +	// getting the account that created it. +	const [ getAccount, getAccountRes ] = useLazyGetAccountQuery(); +	useEffect(() => { +		if (!perm) { +			return; +		} +		getAccount(perm.created_by, true); +	}, [getAccount, perm]); + +	// Load the createdByAccount if possible, +	// returning a username lozenge with +	// a link to the account. +	const createdByAccount = useMemo(() => { +		const { +			data: account, +			isLoading: isLoadingAccount, +			isFetching: isFetchingAccount, +			isError: isErrorAccount, +		} = getAccountRes; +		 +		// Wait for query to finish, returning +		// loading spinner in the meantime. +		if (isLoadingAccount || isFetchingAccount || !perm) { +			return <Loading />; +		} else if (isErrorAccount || account === undefined) { +			// Fall back to account ID. +			return perm?.created_by; +		} + +		return ( +			<Username +				account={account} +				linkTo={`~/settings/moderation/accounts/${account.id}`} +				backLocation={`~${baseUrl}${location}`} +			/> +		); +	}, [getAccountRes, perm, baseUrl, location]); + +	// Now wait til the perm itself is loaded. +	if (isLoadingPerm || isFetchingPerm) { +		return <Loading />; +	} else if (isErrorPerm) { +		return <Error error={errorPerm} />; +	} else if (perm === undefined) { +		throw "perm undefined"; +	} + +	const created = new Date(perm.created_at).toDateString();	 +	 +	// Create parameters to link to regex101 +	// with this regular expression prepopulated. +	const testParams = new URLSearchParams(); +	testParams.set("regex", perm.regex); +	testParams.set("flags", "g"); +	testParams.set("testString", testString); +	const regexLink = `https://regex101.com/?${testParams.toString()}`;	 + +	return ( +		<div className="http-header-permission-details"> +			<h1><BackButton to={`~${baseUrl}/${permType.toLowerCase()}s`} /> HTTP Header {permType} Detail</h1> +			<dl className="info-list"> +				<div className="info-list-entry"> +					<dt>ID</dt> +					<dd>{perm.id}</dd> +				</div> +				<div className="info-list-entry"> +					<dt>Created</dt> +					<dd><time dateTime={perm.created_at}>{created}</time></dd> +				</div> +				<div className="info-list-entry"> +					<dt>Created By</dt> +					<dd>{createdByAccount}</dd> +				</div> +				<div className="info-list-entry"> +					<dt>Header Name</dt> +					<dd>{perm.header}</dd> +				</div> +				<div className="info-list-entry"> +					<dt>Header Value Regex</dt> +					<dd className="monospace">{perm.regex}</dd> +				</div> +				<div className="info-list-entry"> +					<dt>Test This Regex</dt> +					<dd> +						<a +							href={regexLink} +							target="_blank" +							rel="noreferrer" +						> +							<i className="fa fa-fw fa-external-link" aria-hidden="true"></i> Link to Regex101 (opens in a new tab) +						</a> +					</dd> +				</div> +			</dl> +			{ permType === "Block" +				? <DeleteBlock id={perm.id} /> +				: <DeleteAllow id={perm.id} /> +			} +		</div> +	); +} + +function DeleteBlock({ id }: { id: string }) { +	const [ _location, setLocation ] = useLocation(); +	const baseUrl = useBaseUrl(); +	const [ removeTrigger, removeResult ] = useDeleteHeaderBlockMutation(); +	 +	return ( +		<MutationButton +			type="button" +			onClick={() => { +				removeTrigger(id); +				setLocation(`~${baseUrl}/blocks`); +			}} +			label="Remove this block" +			result={removeResult} +			className="button danger" +			showError={false} +			disabled={false} +		/> +	); +} + +function DeleteAllow({ id }: { id: string }) { +	const [ _location, setLocation ] = useLocation(); +	const baseUrl = useBaseUrl(); +	const [ removeTrigger, removeResult ] = useDeleteHeaderAllowMutation(); +	 +	return ( +		<MutationButton +			type="button" +			onClick={() => { +				removeTrigger(id); +				setLocation(`~${baseUrl}/allows`); +			}} +			label="Remove this allow" +			result={removeResult} +			className="button danger" +			showError={false} +			disabled={false} +		/> +	); +} diff --git a/web/source/settings/views/admin/http-header-permissions/overview.tsx b/web/source/settings/views/admin/http-header-permissions/overview.tsx new file mode 100644 index 000000000..7735e624e --- /dev/null +++ b/web/source/settings/views/admin/http-header-permissions/overview.tsx @@ -0,0 +1,169 @@ +/* +	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, { useMemo } from "react"; +import { useGetHeaderAllowsQuery, useGetHeaderBlocksQuery } from "../../../lib/query/admin/http-header-permissions"; +import { NoArg } from "../../../lib/types/query"; +import { PageableList } from "../../../components/pageable-list"; +import { HeaderPermission } from "../../../lib/types/http-header-permissions"; +import { useLocation, useParams } from "wouter"; +import { PermType } from "../../../lib/types/perm"; +import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; +import { SerializedError } from "@reduxjs/toolkit"; +import HeaderPermCreateForm from "./create"; + +export default function HeaderPermsOverview() { +	const [ location, setLocation ] = useLocation(); +	 +	// Parse perm type from routing params. +	let params = useParams(); +	if (params.permType !== "blocks" && params.permType !== "allows") { +		throw "unrecognized perm type " + params.permType; +	} +	const permType = useMemo(() => { +		return params.permType?.slice(0, -1) as PermType; +	}, [params]); + +	// Uppercase first letter of given permType. +	const permTypeUpper = useMemo(() => { +		return permType.charAt(0).toUpperCase() + permType.slice(1);  +	}, [permType]); +	 +	// Fetch desired perms, skipping +	// the ones we don't want. +	const { +		data: blocks, +		isLoading: isLoadingBlocks, +		isFetching: isFetchingBlocks, +		isSuccess: isSuccessBlocks, +		isError: isErrorBlocks, +		error: errorBlocks +	} = useGetHeaderBlocksQuery(NoArg, { skip: permType !== "block" }); + +	const { +		data: allows, +		isLoading: isLoadingAllows, +		isFetching: isFetchingAllows, +		isSuccess: isSuccessAllows, +		isError: isErrorAllows, +		error: errorAllows +	} = useGetHeaderAllowsQuery(NoArg, { skip: permType !== "allow" }); + +	const itemToEntry = (perm: HeaderPermission) => { +		return ( +			<dl +				key={perm.id} +				className="entry spanlink" +				onClick={() => { +					// When clicking on a header perm, +					// go to the detail view for perm. +					setLocation(`/${permType}s/${perm.id}`, { +						// Store the back location in +						// history so the detail view +						// can use it to return here. +						state: { backLocation: location } +					}); +				}} +				role="link" +				tabIndex={0} +			> +				<dt>{perm.header}</dt> +				<dd>{perm.regex}</dd> +			</dl> +		); +	}; + +	const emptyMessage = ( +		<div className="info"> +			<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i> +			<b> +				No HTTP header {permType}s exist yet. +				You can create one using the form below. +			</b> +		</div> +	); + +	let isLoading: boolean; +	let isFetching: boolean; +	let isSuccess: boolean;  +	let isError: boolean; +	let error: FetchBaseQueryError | SerializedError | undefined; +	let items: HeaderPermission[] | undefined; + +	if (permType === "block") { +		isLoading = isLoadingBlocks; +		isFetching = isFetchingBlocks; +		isSuccess = isSuccessBlocks; +		isError = isErrorBlocks; +		error = errorBlocks; +		items = blocks; +	} else { +		isLoading = isLoadingAllows; +		isFetching = isFetchingAllows; +		isSuccess = isSuccessAllows; +		isError = isErrorAllows; +		error = errorAllows; +		items = allows; +	} + +	return ( +		<div className="http-header-permissions"> +			<div className="form-section-docs"> +				<h1>HTTP Header {permTypeUpper}s</h1> +				<p> +					On this page, you can view, create, and remove HTTP header {permType} entries, +					<br/> +					Blocks and allows have different effects depending on the value you've set +					for <code>advanced-header-filter-mode</code> in your instance configuration. +					<br/> +					{ permType === "block" && <> +						<strong> +							When running in <code>block</code> mode, be very careful when creating +							your value regexes, as a too-broad match can cause your instance to +							deny all requests, locking you out of this settings panel. +						</strong> +						<br/> +						If you do this by accident, you can fix it by stopping your instance, +						changing <code>advanced-header-filter-mode</code> to an empty string +						(disabled), starting your instance again, and removing the block. +					</> } +				</p> +				<a +					href="https://docs.gotosocial.org/en/latest/admin/request_filtering_modes/" +					target="_blank" +					className="docslink" +					rel="noreferrer" +				> +					Learn more about HTTP request filtering (opens in a new tab) +				</a> +			</div> +			<PageableList +				isLoading={isLoading} +				isFetching={isFetching} +				isSuccess={isSuccess} +				isError={isError} +				error={error} +				items={items} +				itemToEntry={itemToEntry} +				emptyMessage={emptyMessage} +			/> +			<HeaderPermCreateForm permType={permType} /> +		</div> +	); +} | 
