summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-05-05 13:47:22 +0200
committerLibravatar GitHub <noreply@github.com>2024-05-05 11:47:22 +0000
commit6171dcbe5109d7accbf44f19c20c9f4a0ee5e06f (patch)
tree9011f0050571f5a8c1c0e7bd90b74b2816dadd8a
parent[frontend] Do optimistic update when approving/rejecting/suspending account (... (diff)
downloadgotosocial-6171dcbe5109d7accbf44f19c20c9f4a0ee5e06f.tar.xz
[feature] Add HTTP header permission section to frontend (#2893)
* [feature] Add HTTP header filter section to frontend * tweak naming a bit
-rw-r--r--cmd/gotosocial/action/testrig/testrig.go1
-rw-r--r--docs/admin/request_filtering_modes.md4
-rw-r--r--docs/configuration/advanced.md19
-rw-r--r--example/config.yaml2
-rw-r--r--internal/api/client/admin/headerfilter.go4
-rw-r--r--internal/api/client/admin/headerfilter_delete.go2
-rw-r--r--mkdocs.yml1
-rw-r--r--testrig/config.go1
-rw-r--r--web/source/settings/components/authorization/index.tsx21
-rw-r--r--web/source/settings/components/form/inputs.tsx10
-rw-r--r--web/source/settings/components/pageable-list.tsx2
-rw-r--r--web/source/settings/lib/query/admin/http-header-permissions/index.ts159
-rw-r--r--web/source/settings/lib/query/admin/index.ts1
-rw-r--r--web/source/settings/lib/query/gts-api.ts2
-rw-r--r--web/source/settings/lib/types/domain-permission.ts3
-rw-r--r--web/source/settings/lib/types/http-header-permissions.ts48
-rw-r--r--web/source/settings/lib/types/perm.ts20
-rw-r--r--web/source/settings/style.css106
-rw-r--r--web/source/settings/views/admin/http-header-permissions/create.tsx143
-rw-r--r--web/source/settings/views/admin/http-header-permissions/detail.tsx246
-rw-r--r--web/source/settings/views/admin/http-header-permissions/overview.tsx169
-rw-r--r--web/source/settings/views/admin/menu.tsx27
-rw-r--r--web/source/settings/views/admin/router.tsx45
-rw-r--r--web/source/settings/views/moderation/accounts/pending/index.tsx2
-rw-r--r--web/source/settings/views/moderation/accounts/search/index.tsx2
-rw-r--r--web/source/settings/views/moderation/domain-permissions/detail.tsx3
-rw-r--r--web/source/settings/views/moderation/domain-permissions/overview.tsx3
27 files changed, 982 insertions, 64 deletions
diff --git a/cmd/gotosocial/action/testrig/testrig.go b/cmd/gotosocial/action/testrig/testrig.go
index 79361375b..24d41721a 100644
--- a/cmd/gotosocial/action/testrig/testrig.go
+++ b/cmd/gotosocial/action/testrig/testrig.go
@@ -165,6 +165,7 @@ var Start action.GTSAction = func(ctx context.Context) error {
middlewares = append(middlewares, []gin.HandlerFunc{
middleware.Logger(config.GetLogClientIP()),
+ middleware.HeaderFilter(&state),
middleware.UserAgent(),
middleware.CORS(),
middleware.ExtraHeaders(),
diff --git a/docs/admin/request_filtering_modes.md b/docs/admin/request_filtering_modes.md
index 78f5dfe77..e90a1f3a2 100644
--- a/docs/admin/request_filtering_modes.md
+++ b/docs/admin/request_filtering_modes.md
@@ -7,8 +7,6 @@ GoToSocial currently offers 'block', 'allow' and disabled HTTP request header fi
HTTP request header filtering is also still considered "experimental". It should do what it says on the box, but it may cause bugs or edge cases to appear elsewhere, we're not sure yet!
- Management via settings panel is TBA. Until then you will need to manage these directly via API endpoints.
-
## Disabled header filtering mode (default)
When `advanced-header-filter-mode` is set to `""`, i.e. an empty string, all request header filtering will be disabled.
@@ -30,4 +28,4 @@ In allow mode, a block header filter can be used to override an existing allow f
A request in allow mode will only be accepted if it is EXPLICITLY ALLOWED AND NOT EXPLICITLY BLOCKED.
!!! danger
- Allow filtering mode is an extremely restrictive mode that will almost certainly prevent many (legitimate) clients from being able to access your instance, including yourself. You should only enable this mode if you know exactly what you're trying to achieve. \ No newline at end of file
+ Allow filtering mode is an extremely restrictive mode that will almost certainly prevent many (legitimate) clients from being able to access your instance, including yourself. You should only enable this mode if you know exactly what you're trying to achieve.
diff --git a/docs/configuration/advanced.md b/docs/configuration/advanced.md
index b97d8a6ba..88f4aff67 100644
--- a/docs/configuration/advanced.md
+++ b/docs/configuration/advanced.md
@@ -163,4 +163,23 @@ advanced-sender-multiplier: 2
# Example: ["s3.example.org", "some-bucket-name.s3.example.org"]
# Default: []
advanced-csp-extra-uris: []
+
+# String. HTTP request header filtering mode to use for this instance.
+#
+# "block" -- only requests that are explicitly blocked by header filters
+# will be denied (unless they are also explicitly allowed).
+#
+# "allow" -- only requests that are explicitly allowed by header filters
+# will be accepted (unless they are also explicitly blocked).
+# This mode is considered experimental and will almost certainly
+# break access to your instance unless you are very careful.
+#
+# "" -- request header filtering disabled.
+#
+# For more details on block and allow modes, check the documentation at:
+# https://docs.gotosocial.org/en/latest/admin/request_filtering_modes
+#
+# Options: ["block", "allow", ""]
+# Default: ""
+advanced-header-filter-mode: ""
```
diff --git a/example/config.yaml b/example/config.yaml
index b5e0fe57f..6ef9f7a6c 100644
--- a/example/config.yaml
+++ b/example/config.yaml
@@ -1090,6 +1090,8 @@ advanced-csp-extra-uris: []
#
# "allow" -- only requests that are explicitly allowed by header filters
# will be accepted (unless they are also explicitly blocked).
+# This mode is considered experimental and will almost certainly
+# break access to your instance unless you are very careful.
#
# "" -- request header filtering disabled.
#
diff --git a/internal/api/client/admin/headerfilter.go b/internal/api/client/admin/headerfilter.go
index 01bcaca16..d3dad5917 100644
--- a/internal/api/client/admin/headerfilter.go
+++ b/internal/api/client/admin/headerfilter.go
@@ -52,7 +52,7 @@ func (m *Module) getHeaderFilter(c *gin.Context, get func(context.Context, strin
return
}
- filterID, errWithCode := apiutil.ParseID(c.Param("ID"))
+ filterID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
@@ -167,7 +167,7 @@ func (m *Module) deleteHeaderFilter(c *gin.Context, delete func(context.Context,
return
}
- filterID, errWithCode := apiutil.ParseID(c.Param("ID"))
+ filterID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/headerfilter_delete.go b/internal/api/client/admin/headerfilter_delete.go
index 806e62a04..58b1c585e 100644
--- a/internal/api/client/admin/headerfilter_delete.go
+++ b/internal/api/client/admin/headerfilter_delete.go
@@ -92,5 +92,5 @@ func (m *Module) HeaderFilterAllowDELETE(c *gin.Context) {
// '500':
// description: internal server error
func (m *Module) HeaderFilterBlockDELETE(c *gin.Context) {
- m.deleteHeaderFilter(c, m.processor.Admin().DeleteAllowHeaderFilter)
+ m.deleteHeaderFilter(c, m.processor.Admin().DeleteBlockHeaderFilter)
}
diff --git a/mkdocs.yml b/mkdocs.yml
index 8d7ecc65a..c0132fb2f 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -118,6 +118,7 @@ nav:
- "admin/signups.md"
- "admin/federation_modes.md"
- "admin/domain_blocks.md"
+ - "admin/request_filtering_modes.md"
- "admin/robots.md"
- "admin/cli.md"
- "admin/backup_and_restore.md"
diff --git a/testrig/config.go b/testrig/config.go
index 2717b733e..93f3c5523 100644
--- a/testrig/config.go
+++ b/testrig/config.go
@@ -146,6 +146,7 @@ var testDefaults = config.Configuration{
AdvancedRateLimitRequests: 0, // disabled
AdvancedThrottlingMultiplier: 0, // disabled
AdvancedSenderMultiplier: 0, // 1 sender only, regardless of CPU
+ AdvancedHeaderFilterMode: config.RequestHeaderFilterModeBlock,
SoftwareVersion: "0.0.0-testrig",
diff --git a/web/source/settings/components/authorization/index.tsx b/web/source/settings/components/authorization/index.tsx
index 22a0d24b7..e8f4d6673 100644
--- a/web/source/settings/components/authorization/index.tsx
+++ b/web/source/settings/components/authorization/index.tsx
@@ -17,10 +17,9 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import { useVerifyCredentialsQuery } from "../../lib/query/oauth";
+import { useLogoutMutation, useVerifyCredentialsQuery } from "../../lib/query/oauth";
import { store } from "../../redux/store";
-
-import React from "react";
+import React, { ReactNode } from "react";
import Login from "./login";
import Loading from "../loading";
@@ -30,18 +29,20 @@ import { NoArg } from "../../lib/types/query";
export function Authorization({ App }) {
const { loginState, expectingRedirect } = store.getState().oauth;
const skip = (loginState == "none" || loginState == "logout" || expectingRedirect);
+ const [ logoutQuery ] = useLogoutMutation();
const {
isLoading,
+ isFetching,
isSuccess,
data: account,
error,
} = useVerifyCredentialsQuery(NoArg, { skip: skip });
let showLogin = true;
- let content: React.JSX.Element | null = null;
+ let content: ReactNode;
- if (isLoading) {
+ if (isLoading || isFetching) {
showLogin = false;
let loadingInfo = "";
@@ -56,7 +57,15 @@ export function Authorization({ App }) {
<Loading /> {loadingInfo}
</div>
);
- } else if (error != undefined) {
+ } else if (error !== undefined) {
+ if ("status" in error && error.status === 401) {
+ // 401 unauthorized was received.
+ // That means the token or app we
+ // were using is no longer valid,
+ // so just log the user out.
+ logoutQuery(NoArg);
+ }
+
content = (
<div>
<Error error={error} />
diff --git a/web/source/settings/components/form/inputs.tsx b/web/source/settings/components/form/inputs.tsx
index 1e0d8eaab..f82937fc1 100644
--- a/web/source/settings/components/form/inputs.tsx
+++ b/web/source/settings/components/form/inputs.tsx
@@ -34,7 +34,7 @@ export interface TextInputProps extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
- label?: string;
+ label?: ReactNode;
field: TextFormInputHook;
}
@@ -60,7 +60,7 @@ export interface TextAreaProps extends React.DetailedHTMLProps<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
> {
- label?: string;
+ label?: ReactNode;
field: TextFormInputHook;
}
@@ -86,7 +86,7 @@ export interface FileInputProps extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
- label?: string;
+ label?: ReactNode;
field: FileFormInputHook;
}
@@ -133,7 +133,7 @@ export interface SelectProps extends React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
> {
- label?: string;
+ label?: ReactNode;
field: TextFormInputHook;
children?: ReactNode;
options: React.JSX.Element;
@@ -164,7 +164,7 @@ export interface RadioGroupProps extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
- label?: string;
+ label?: ReactNode;
field: RadioFormInputHook;
}
diff --git a/web/source/settings/components/pageable-list.tsx b/web/source/settings/components/pageable-list.tsx
index 918103ead..3571fb1cd 100644
--- a/web/source/settings/components/pageable-list.tsx
+++ b/web/source/settings/components/pageable-list.tsx
@@ -33,7 +33,7 @@ export interface PageableListProps<T> {
isFetching: boolean;
isError: boolean;
error: FetchBaseQueryError | SerializedError | undefined;
- emptyMessage: string;
+ emptyMessage: ReactNode;
prevNextLinks?: Links | null | undefined;
}
diff --git a/web/source/settings/lib/query/admin/http-header-permissions/index.ts b/web/source/settings/lib/query/admin/http-header-permissions/index.ts
new file mode 100644
index 000000000..342b9eb56
--- /dev/null
+++ b/web/source/settings/lib/query/admin/http-header-permissions/index.ts
@@ -0,0 +1,159 @@
+/*
+ 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 { gtsApi } from "../../gts-api";
+import { HeaderPermission } from "../../../types/http-header-permissions";
+
+const extended = gtsApi.injectEndpoints({
+ endpoints: (build) => ({
+
+ /* HTTP HEADER ALLOWS */
+
+ getHeaderAllows: build.query<HeaderPermission[], void>({
+ query: () => ({
+ url: `/api/v1/admin/header_allows`
+ }),
+ providesTags: (res) =>
+ res
+ ? [
+ ...res.map(({ id }) => ({ type: "HTTPHeaderAllows" as const, id })),
+ { type: "HTTPHeaderAllows", id: "LIST" },
+ ]
+ : [{ type: "HTTPHeaderAllows", id: "LIST" }],
+ }),
+
+ getHeaderAllow: build.query<HeaderPermission, string>({
+ query: (id) => ({
+ url: `/api/v1/admin/header_allows/${id}`
+ }),
+ providesTags: (_res, _error, id) => [{ type: "HTTPHeaderAllows", id }],
+ }),
+
+ postHeaderAllow: build.mutation<HeaderPermission, { header: string, regex: string }>({
+ query: (formData) => ({
+ method: "POST",
+ url: `/api/v1/admin/header_allows`,
+ asForm: true,
+ body: formData,
+ discardEmpty: true
+ }),
+ invalidatesTags: [{ type: "HTTPHeaderAllows", id: "LIST" }],
+ }),
+
+ deleteHeaderAllow: build.mutation<HeaderPermission, string>({
+ query: (id) => ({
+ method: "DELETE",
+ url: `/api/v1/admin/header_allows/${id}`
+ }),
+ invalidatesTags: (_res, _error, id) => [{ type: "HTTPHeaderAllows", id }],
+ }),
+
+ /* HTTP HEADER BLOCKS */
+
+ getHeaderBlocks: build.query<HeaderPermission[], void>({
+ query: () => ({
+ url: `/api/v1/admin/header_blocks`
+ }),
+ providesTags: (res) =>
+ res
+ ? [
+ ...res.map(({ id }) => ({ type: "HTTPHeaderBlocks" as const, id })),
+ { type: "HTTPHeaderBlocks", id: "LIST" },
+ ]
+ : [{ type: "HTTPHeaderBlocks", id: "LIST" }],
+ }),
+
+ postHeaderBlock: build.mutation<HeaderPermission, { header: string, regex: string }>({
+ query: (formData) => ({
+ method: "POST",
+ url: `/api/v1/admin/header_blocks`,
+ asForm: true,
+ body: formData,
+ discardEmpty: true
+ }),
+ invalidatesTags: [{ type: "HTTPHeaderBlocks", id: "LIST" }],
+ }),
+
+ getHeaderBlock: build.query<HeaderPermission, string>({
+ query: (id) => ({
+ url: `/api/v1/admin/header_blocks/${id}`
+ }),
+ providesTags: (_res, _error, id) => [{ type: "HTTPHeaderBlocks", id }],
+ }),
+
+ deleteHeaderBlock: build.mutation<HeaderPermission, string>({
+ query: (id) => ({
+ method: "DELETE",
+ url: `/api/v1/admin/header_blocks/${id}`
+ }),
+ invalidatesTags: (_res, _error, id) => [{ type: "HTTPHeaderBlocks", id }],
+ }),
+ }),
+});
+
+/**
+ * Get admin view of all HTTP header allow regexes.
+ */
+const useGetHeaderAllowsQuery = extended.useGetHeaderAllowsQuery;
+
+/**
+ * Get admin view of one HTTP header allow regex.
+ */
+const useGetHeaderAllowQuery = extended.useGetHeaderAllowQuery;
+
+/**
+ * Create a new HTTP header allow regex.
+ */
+const usePostHeaderAllowMutation = extended.usePostHeaderAllowMutation;
+
+/**
+ * Delete one HTTP header allow regex.
+ */
+const useDeleteHeaderAllowMutation = extended.useDeleteHeaderAllowMutation;
+
+/**
+ * Get admin view of all HTTP header block regexes.
+ */
+const useGetHeaderBlocksQuery = extended.useGetHeaderBlocksQuery;
+
+/**
+ * Get admin view of one HTTP header block regex.
+ */
+const useGetHeaderBlockQuery = extended.useGetHeaderBlockQuery;
+
+/**
+ * Create a new HTTP header block regex.
+ */
+const usePostHeaderBlockMutation = extended.usePostHeaderBlockMutation;
+
+/**
+ * Delete one HTTP header block regex.
+ */
+const useDeleteHeaderBlockMutation = extended.useDeleteHeaderBlockMutation;
+
+export {
+ useGetHeaderAllowsQuery,
+ useGetHeaderAllowQuery,
+ usePostHeaderAllowMutation,
+ useDeleteHeaderAllowMutation,
+ useGetHeaderBlocksQuery,
+ useGetHeaderBlockQuery,
+ usePostHeaderBlockMutation,
+ useDeleteHeaderBlockMutation,
+};
diff --git a/web/source/settings/lib/query/admin/index.ts b/web/source/settings/lib/query/admin/index.ts
index fd3e1ca1b..3a095a8a1 100644
--- a/web/source/settings/lib/query/admin/index.ts
+++ b/web/source/settings/lib/query/admin/index.ts
@@ -217,6 +217,7 @@ export const {
useMediaCleanupMutation,
useInstanceKeysExpireMutation,
useGetAccountQuery,
+ useLazyGetAccountQuery,
useActionAccountMutation,
useSearchAccountsQuery,
useLazySearchAccountsQuery,
diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts
index 6e5eafeab..ef994e655 100644
--- a/web/source/settings/lib/query/gts-api.ts
+++ b/web/source/settings/lib/query/gts-api.ts
@@ -139,6 +139,8 @@ export const gtsApi = createApi({
"Reports",
"Account",
"InstanceRules",
+ "HTTPHeaderAllows",
+ "HTTPHeaderBlocks",
],
endpoints: (build) => ({
instanceV1: build.query<InstanceV1, void>({
diff --git a/web/source/settings/lib/types/domain-permission.ts b/web/source/settings/lib/types/domain-permission.ts
index 93df883f2..ccf7c9c57 100644
--- a/web/source/settings/lib/types/domain-permission.ts
+++ b/web/source/settings/lib/types/domain-permission.ts
@@ -18,11 +18,10 @@
*/
import typia from "typia";
+import { PermType } from "./perm";
export const validateDomainPerms = typia.createValidate<DomainPerm[]>();
-export type PermType = "block" | "allow";
-
/**
* A single domain permission entry (block or allow).
*/
diff --git a/web/source/settings/lib/types/http-header-permissions.ts b/web/source/settings/lib/types/http-header-permissions.ts
new file mode 100644
index 000000000..7e3aade27
--- /dev/null
+++ b/web/source/settings/lib/types/http-header-permissions.ts
@@ -0,0 +1,48 @@
+/*
+ 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/>.
+*/
+
+export interface HeaderPermission {
+ /**
+ * ID of this entry.
+ */
+ id: string;
+
+ /**
+ * HTTP header key to match on.
+ */
+ header: string;
+
+ /**
+ * ISO8601 timestamp when
+ * this entry was created.
+ */
+ created_at: string;
+
+ /**
+ * ID of the account that
+ * created this entry.
+ */
+ created_by: string;
+
+ /**
+ * Regular expression to match
+ * on when allowing/blocking.
+ */
+ regex: string;
+}
diff --git a/web/source/settings/lib/types/perm.ts b/web/source/settings/lib/types/perm.ts
new file mode 100644
index 000000000..2f4073c90
--- /dev/null
+++ b/web/source/settings/lib/types/perm.ts
@@ -0,0 +1,20 @@
+/*
+ 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/>.
+*/
+
+export type PermType = "block" | "allow";
diff --git a/web/source/settings/style.css b/web/source/settings/style.css
index 01263c224..b73978e33 100644
--- a/web/source/settings/style.css
+++ b/web/source/settings/style.css
@@ -60,7 +60,6 @@ ul li::before {
& > form {
display: flex;
flex-direction: column;
- gap: 0.5rem;
margin: 1rem 0;
h1, h2, h3, h4, h5 {
@@ -1192,47 +1191,47 @@ button.with-padding {
margin-bottom: 0;
}
- .info-list {
- border: 0.1rem solid $gray1;
+ .action-buttons {
display: flex;
- flex-direction: column;
+ gap: 0.5rem;
+ align-items: center;
+ }
+}
- .info-list-entry {
- background: $list-entry-bg;
- border: 0.1rem solid transparent;
- padding: 0.25rem;
+.info-list {
+ border: 0.1rem solid $gray1;
+ display: flex;
+ flex-direction: column;
- &:nth-child(even) {
- background: $list-entry-alternate-bg;
- }
+ .info-list-entry {
+ background: $list-entry-bg;
+ border: 0.1rem solid transparent;
+ padding: 0.25rem;
- display: grid;
- grid-template-columns: max(20%, 10rem) 1fr;
-
- dt {
- font-weight: bold;
- }
+ &:nth-child(even) {
+ background: $list-entry-alternate-bg;
+ }
- dd {
- word-break: break-word;
- }
+ display: grid;
+ grid-template-columns: max(20%, 10rem) 1fr;
+
+ dt {
+ font-weight: bold;
+ }
- dt, dd {
- /*
- Make sure any fa icons used in keys
- or values are properly aligned.
- */
- .fa {
- vertical-align: middle;
- }
- }
+ dd {
+ word-break: break-word;
}
- }
- .action-buttons {
- display: flex;
- gap: 0.5rem;
- align-items: center;
+ dt, dd {
+ /*
+ Make sure any fa icons used in keys
+ or values are properly aligned.
+ */
+ .fa {
+ vertical-align: middle;
+ }
+ }
}
}
@@ -1287,6 +1286,45 @@ button.with-padding {
}
}
+.http-header-permissions {
+ .list {
+ /*
+ Space this page out a bit, it
+ looks too tight otherwise.
+ */
+ margin: 1rem 0;
+
+ /*
+ Visually separate the key + value for
+ each entry, and show the value in
+ reasonably-sized monospace font.
+ */
+ .entries > .entry {
+ display: grid;
+ grid-template-columns: max(20%, 10rem) 1fr;
+
+ dt {
+ font-weight: bold;
+ }
+
+ dd {
+ font-family: monospace;
+ font-size: large;
+ }
+ }
+ }
+}
+
+.http-header-permission-details {
+ .info-list {
+ margin-top: 1rem;
+
+ > .info-list-entry > .monospace {
+ font-size: large;
+ }
+ }
+}
+
@media screen and (orientation: portrait) {
.reports .report .byline {
grid-template-columns: 1fr;
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&nbsp;
+ <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&nbsp;
+ <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>
+ );
+}
diff --git a/web/source/settings/views/admin/menu.tsx b/web/source/settings/views/admin/menu.tsx
index 2cf5a35c2..481f51a4d 100644
--- a/web/source/settings/views/admin/menu.tsx
+++ b/web/source/settings/views/admin/menu.tsx
@@ -36,6 +36,10 @@ import { useHasPermission } from "../../lib/navigation/util";
* - /settings/admin/actions
* - /settings/admin/actions/media
* - /settings/admin/actions/keys
+ * - /settings/admin/http-header-permissions/blocks
+ * - /settings/admin/http-header-permissions/blocks/:blockId\
+ * - /settings/admin/http-header-permissions/allows
+ * - /settings/admin/http-header-permissions/allows/:allowId
*/
export default function AdminMenu() {
const permissions = ["admin"];
@@ -54,6 +58,7 @@ export default function AdminMenu() {
<AdminInstanceMenu />
<AdminEmojisMenu />
<AdminActionsMenu />
+ <AdminHTTPHeaderPermissionsMenu />
</MenuItem>
);
}
@@ -127,3 +132,25 @@ function AdminEmojisMenu() {
</MenuItem>
);
}
+
+function AdminHTTPHeaderPermissionsMenu() {
+ return (
+ <MenuItem
+ name="HTTP Header Permissions"
+ itemUrl="http-header-permissions"
+ defaultChild="blocks"
+ icon="fa-hubzilla"
+ >
+ <MenuItem
+ name="Blocks"
+ itemUrl="blocks"
+ icon="fa-close"
+ />
+ <MenuItem
+ name="Allows"
+ itemUrl="allows"
+ icon="fa-check"
+ />
+ </MenuItem>
+ );
+}
diff --git a/web/source/settings/views/admin/router.tsx b/web/source/settings/views/admin/router.tsx
index 95d146510..68c4a5ef3 100644
--- a/web/source/settings/views/admin/router.tsx
+++ b/web/source/settings/views/admin/router.tsx
@@ -29,15 +29,17 @@ import Keys from "./actions/keys";
import EmojiOverview from "./emoji/local/overview";
import EmojiDetail from "./emoji/local/detail";
import RemoteEmoji from "./emoji/remote";
+import HeaderPermsOverview from "./http-header-permissions/overview";
+import HeaderPermDetail from "./http-header-permissions/detail";
/*
EXPORTED COMPONENTS
*/
/**
- * - /settings/instance/settings
- * - /settings/instance/rules
- * - /settings/instance/rules/:ruleId
+ * - /settings/admin/instance/settings
+ * - /settings/admin/instance/rules
+ * - /settings/admin/instance/rules/:ruleId
* - /settings/admin/emojis
* - /settings/admin/emojis/local
* - /settings/admin/emojis/local/:emojiId
@@ -45,6 +47,10 @@ import RemoteEmoji from "./emoji/remote";
* - /settings/admin/actions
* - /settings/admin/actions/media
* - /settings/admin/actions/keys
+ * - /settings/admin/http-header-permissions/allows
+ * - /settings/admin/http-header-permissions/allows/:allowId
+ * - /settings/admin/http-header-permissions/blocks
+ * - /settings/admin/http-header-permissions/blocks/:blockId
*/
export default function AdminRouter() {
const parentUrl = useBaseUrl();
@@ -57,6 +63,7 @@ export default function AdminRouter() {
<AdminInstanceRouter />
<AdminEmojisRouter />
<AdminActionsRouter />
+ <AdminHTTPHeaderPermissionsRouter />
</Router>
</BaseUrlContext.Provider>
);
@@ -125,9 +132,9 @@ function AdminActionsRouter() {
}
/**
- * - /settings/instance/settings
- * - /settings/instance/rules
- * - /settings/instance/rules/:ruleId
+ * - /settings/admin/instance/settings
+ * - /settings/admin/instance/rules
+ * - /settings/admin/instance/rules/:ruleId
*/
function AdminInstanceRouter() {
const parentUrl = useBaseUrl();
@@ -149,3 +156,29 @@ function AdminInstanceRouter() {
</BaseUrlContext.Provider>
);
}
+
+/**
+ * - /settings/admin/http-header-permissions/blocks
+ * - /settings/admin/http-header-permissions/blocks/:blockId
+ * - /settings/admin/http-header-permissions/allows
+ * - /settings/admin/http-header-permissions/allows/:allowId
+ */
+function AdminHTTPHeaderPermissionsRouter() {
+ const parentUrl = useBaseUrl();
+ const thisBase = "/http-header-permissions";
+ const absBase = parentUrl + thisBase;
+
+ return (
+ <BaseUrlContext.Provider value={absBase}>
+ <Router base={thisBase}>
+ <ErrorBoundary>
+ <Switch>
+ <Route path="/:permType" component={HeaderPermsOverview} />
+ <Route path="/:permType/:permId" component={HeaderPermDetail} />
+ <Route><Redirect to="/blocks" /></Route>
+ </Switch>
+ </ErrorBoundary>
+ </Router>
+ </BaseUrlContext.Provider>
+ );
+}
diff --git a/web/source/settings/views/moderation/accounts/pending/index.tsx b/web/source/settings/views/moderation/accounts/pending/index.tsx
index b72de52bf..d47363954 100644
--- a/web/source/settings/views/moderation/accounts/pending/index.tsx
+++ b/web/source/settings/views/moderation/accounts/pending/index.tsx
@@ -53,7 +53,7 @@ export default function AccountsPending() {
itemToEntry={itemToEntry}
isError={searchRes.isError}
error={searchRes.error}
- emptyMessage="No pending account sign-ups."
+ emptyMessage={<b>No pending account sign-ups.</b>}
/>
</div>
);
diff --git a/web/source/settings/views/moderation/accounts/search/index.tsx b/web/source/settings/views/moderation/accounts/search/index.tsx
index 58e2d6505..16e89ce43 100644
--- a/web/source/settings/views/moderation/accounts/search/index.tsx
+++ b/web/source/settings/views/moderation/accounts/search/index.tsx
@@ -166,7 +166,7 @@ export function AccountSearchForm() {
itemToEntry={itemToEntry}
isError={searchRes.isError}
error={searchRes.error}
- emptyMessage="No accounts found that match your query"
+ emptyMessage={<b>No accounts found that match your query.</b>}
prevNextLinks={searchRes.data?.links}
/>
</>
diff --git a/web/source/settings/views/moderation/domain-permissions/detail.tsx b/web/source/settings/views/moderation/domain-permissions/detail.tsx
index b9d439aee..47072b8cb 100644
--- a/web/source/settings/views/moderation/domain-permissions/detail.tsx
+++ b/web/source/settings/views/moderation/domain-permissions/detail.tsx
@@ -34,10 +34,11 @@ 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 { DomainPerm } from "../../../lib/types/domain-permission";
import { NoArg } from "../../../lib/types/query";
import { Error } from "../../../components/error";
import { useBaseUrl } from "../../../lib/navigation/util";
+import { PermType } from "../../../lib/types/perm";
export default function DomainPermDetail() {
const baseUrl = useBaseUrl();
diff --git a/web/source/settings/views/moderation/domain-permissions/overview.tsx b/web/source/settings/views/moderation/domain-permissions/overview.tsx
index fca8e348e..645c51e93 100644
--- a/web/source/settings/views/moderation/domain-permissions/overview.tsx
+++ b/web/source/settings/views/moderation/domain-permissions/overview.tsx
@@ -26,8 +26,9 @@ 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 type { MappedDomainPerms } from "../../../lib/types/domain-permission";
import { NoArg } from "../../../lib/types/query";
+import { PermType } from "../../../lib/types/perm";
export default function DomainPermissionsOverview() {
// Parse perm type from routing params.