mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-25 21:26:40 +00:00
[feature] Add HTTP header permission section to frontend (#2893)
* [feature] Add HTTP header filter section to frontend * tweak naming a bit
This commit is contained in:
parent
35b1c54bde
commit
6171dcbe51
|
@ -165,6 +165,7 @@
|
||||||
|
|
||||||
middlewares = append(middlewares, []gin.HandlerFunc{
|
middlewares = append(middlewares, []gin.HandlerFunc{
|
||||||
middleware.Logger(config.GetLogClientIP()),
|
middleware.Logger(config.GetLogClientIP()),
|
||||||
|
middleware.HeaderFilter(&state),
|
||||||
middleware.UserAgent(),
|
middleware.UserAgent(),
|
||||||
middleware.CORS(),
|
middleware.CORS(),
|
||||||
middleware.ExtraHeaders(),
|
middleware.ExtraHeaders(),
|
||||||
|
|
|
@ -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!
|
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)
|
## 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.
|
When `advanced-header-filter-mode` is set to `""`, i.e. an empty string, all request header filtering will be disabled.
|
||||||
|
|
|
@ -163,4 +163,23 @@ advanced-sender-multiplier: 2
|
||||||
# Example: ["s3.example.org", "some-bucket-name.s3.example.org"]
|
# Example: ["s3.example.org", "some-bucket-name.s3.example.org"]
|
||||||
# Default: []
|
# Default: []
|
||||||
advanced-csp-extra-uris: []
|
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: ""
|
||||||
```
|
```
|
||||||
|
|
|
@ -1090,6 +1090,8 @@ advanced-csp-extra-uris: []
|
||||||
#
|
#
|
||||||
# "allow" -- only requests that are explicitly allowed by header filters
|
# "allow" -- only requests that are explicitly allowed by header filters
|
||||||
# will be accepted (unless they are also explicitly blocked).
|
# 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.
|
# "" -- request header filtering disabled.
|
||||||
#
|
#
|
||||||
|
|
|
@ -52,7 +52,7 @@ func (m *Module) getHeaderFilter(c *gin.Context, get func(context.Context, strin
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filterID, errWithCode := apiutil.ParseID(c.Param("ID"))
|
filterID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
|
@ -167,7 +167,7 @@ func (m *Module) deleteHeaderFilter(c *gin.Context, delete func(context.Context,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filterID, errWithCode := apiutil.ParseID(c.Param("ID"))
|
filterID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
|
|
|
@ -92,5 +92,5 @@ func (m *Module) HeaderFilterAllowDELETE(c *gin.Context) {
|
||||||
// '500':
|
// '500':
|
||||||
// description: internal server error
|
// description: internal server error
|
||||||
func (m *Module) HeaderFilterBlockDELETE(c *gin.Context) {
|
func (m *Module) HeaderFilterBlockDELETE(c *gin.Context) {
|
||||||
m.deleteHeaderFilter(c, m.processor.Admin().DeleteAllowHeaderFilter)
|
m.deleteHeaderFilter(c, m.processor.Admin().DeleteBlockHeaderFilter)
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,7 @@ nav:
|
||||||
- "admin/signups.md"
|
- "admin/signups.md"
|
||||||
- "admin/federation_modes.md"
|
- "admin/federation_modes.md"
|
||||||
- "admin/domain_blocks.md"
|
- "admin/domain_blocks.md"
|
||||||
|
- "admin/request_filtering_modes.md"
|
||||||
- "admin/robots.md"
|
- "admin/robots.md"
|
||||||
- "admin/cli.md"
|
- "admin/cli.md"
|
||||||
- "admin/backup_and_restore.md"
|
- "admin/backup_and_restore.md"
|
||||||
|
|
|
@ -146,6 +146,7 @@ func InitTestConfig() {
|
||||||
AdvancedRateLimitRequests: 0, // disabled
|
AdvancedRateLimitRequests: 0, // disabled
|
||||||
AdvancedThrottlingMultiplier: 0, // disabled
|
AdvancedThrottlingMultiplier: 0, // disabled
|
||||||
AdvancedSenderMultiplier: 0, // 1 sender only, regardless of CPU
|
AdvancedSenderMultiplier: 0, // 1 sender only, regardless of CPU
|
||||||
|
AdvancedHeaderFilterMode: config.RequestHeaderFilterModeBlock,
|
||||||
|
|
||||||
SoftwareVersion: "0.0.0-testrig",
|
SoftwareVersion: "0.0.0-testrig",
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,9 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
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 { store } from "../../redux/store";
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import Login from "./login";
|
import Login from "./login";
|
||||||
import Loading from "../loading";
|
import Loading from "../loading";
|
||||||
|
@ -30,18 +29,20 @@ import { NoArg } from "../../lib/types/query";
|
||||||
export function Authorization({ App }) {
|
export function Authorization({ App }) {
|
||||||
const { loginState, expectingRedirect } = store.getState().oauth;
|
const { loginState, expectingRedirect } = store.getState().oauth;
|
||||||
const skip = (loginState == "none" || loginState == "logout" || expectingRedirect);
|
const skip = (loginState == "none" || loginState == "logout" || expectingRedirect);
|
||||||
|
const [ logoutQuery ] = useLogoutMutation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isFetching,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
data: account,
|
data: account,
|
||||||
error,
|
error,
|
||||||
} = useVerifyCredentialsQuery(NoArg, { skip: skip });
|
} = useVerifyCredentialsQuery(NoArg, { skip: skip });
|
||||||
|
|
||||||
let showLogin = true;
|
let showLogin = true;
|
||||||
let content: React.JSX.Element | null = null;
|
let content: ReactNode;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || isFetching) {
|
||||||
showLogin = false;
|
showLogin = false;
|
||||||
|
|
||||||
let loadingInfo = "";
|
let loadingInfo = "";
|
||||||
|
@ -56,7 +57,15 @@ export function Authorization({ App }) {
|
||||||
<Loading /> {loadingInfo}
|
<Loading /> {loadingInfo}
|
||||||
</div>
|
</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 = (
|
content = (
|
||||||
<div>
|
<div>
|
||||||
<Error error={error} />
|
<Error error={error} />
|
||||||
|
|
|
@ -34,7 +34,7 @@ export interface TextInputProps extends React.DetailedHTMLProps<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> {
|
> {
|
||||||
label?: string;
|
label?: ReactNode;
|
||||||
field: TextFormInputHook;
|
field: TextFormInputHook;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ export interface TextAreaProps extends React.DetailedHTMLProps<
|
||||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||||
HTMLTextAreaElement
|
HTMLTextAreaElement
|
||||||
> {
|
> {
|
||||||
label?: string;
|
label?: ReactNode;
|
||||||
field: TextFormInputHook;
|
field: TextFormInputHook;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ export interface FileInputProps extends React.DetailedHTMLProps<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> {
|
> {
|
||||||
label?: string;
|
label?: ReactNode;
|
||||||
field: FileFormInputHook;
|
field: FileFormInputHook;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ export interface SelectProps extends React.DetailedHTMLProps<
|
||||||
React.SelectHTMLAttributes<HTMLSelectElement>,
|
React.SelectHTMLAttributes<HTMLSelectElement>,
|
||||||
HTMLSelectElement
|
HTMLSelectElement
|
||||||
> {
|
> {
|
||||||
label?: string;
|
label?: ReactNode;
|
||||||
field: TextFormInputHook;
|
field: TextFormInputHook;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
options: React.JSX.Element;
|
options: React.JSX.Element;
|
||||||
|
@ -164,7 +164,7 @@ export interface RadioGroupProps extends React.DetailedHTMLProps<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> {
|
> {
|
||||||
label?: string;
|
label?: ReactNode;
|
||||||
field: RadioFormInputHook;
|
field: RadioFormInputHook;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ export interface PageableListProps<T> {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
error: FetchBaseQueryError | SerializedError | undefined;
|
error: FetchBaseQueryError | SerializedError | undefined;
|
||||||
emptyMessage: string;
|
emptyMessage: ReactNode;
|
||||||
prevNextLinks?: Links | null | undefined;
|
prevNextLinks?: Links | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
|
@ -217,6 +217,7 @@ export const {
|
||||||
useMediaCleanupMutation,
|
useMediaCleanupMutation,
|
||||||
useInstanceKeysExpireMutation,
|
useInstanceKeysExpireMutation,
|
||||||
useGetAccountQuery,
|
useGetAccountQuery,
|
||||||
|
useLazyGetAccountQuery,
|
||||||
useActionAccountMutation,
|
useActionAccountMutation,
|
||||||
useSearchAccountsQuery,
|
useSearchAccountsQuery,
|
||||||
useLazySearchAccountsQuery,
|
useLazySearchAccountsQuery,
|
||||||
|
|
|
@ -139,6 +139,8 @@ export const gtsApi = createApi({
|
||||||
"Reports",
|
"Reports",
|
||||||
"Account",
|
"Account",
|
||||||
"InstanceRules",
|
"InstanceRules",
|
||||||
|
"HTTPHeaderAllows",
|
||||||
|
"HTTPHeaderBlocks",
|
||||||
],
|
],
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
instanceV1: build.query<InstanceV1, void>({
|
instanceV1: build.query<InstanceV1, void>({
|
||||||
|
|
|
@ -18,11 +18,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import typia from "typia";
|
import typia from "typia";
|
||||||
|
import { PermType } from "./perm";
|
||||||
|
|
||||||
export const validateDomainPerms = typia.createValidate<DomainPerm[]>();
|
export const validateDomainPerms = typia.createValidate<DomainPerm[]>();
|
||||||
|
|
||||||
export type PermType = "block" | "allow";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single domain permission entry (block or allow).
|
* A single domain permission entry (block or allow).
|
||||||
*/
|
*/
|
||||||
|
|
48
web/source/settings/lib/types/http-header-permissions.ts
Normal file
48
web/source/settings/lib/types/http-header-permissions.ts
Normal file
|
@ -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;
|
||||||
|
}
|
20
web/source/settings/lib/types/perm.ts
Normal file
20
web/source/settings/lib/types/perm.ts
Normal file
|
@ -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";
|
|
@ -60,7 +60,6 @@ ul li::before {
|
||||||
& > form {
|
& > form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
|
||||||
h1, h2, h3, h4, h5 {
|
h1, h2, h3, h4, h5 {
|
||||||
|
@ -1192,43 +1191,6 @@ button.with-padding {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-list {
|
|
||||||
border: 0.1rem solid $gray1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.info-list-entry {
|
|
||||||
background: $list-entry-bg;
|
|
||||||
border: 0.1rem solid transparent;
|
|
||||||
padding: 0.25rem;
|
|
||||||
|
|
||||||
&:nth-child(even) {
|
|
||||||
background: $list-entry-alternate-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: max(20%, 10rem) 1fr;
|
|
||||||
|
|
||||||
dt {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd {
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt, dd {
|
|
||||||
/*
|
|
||||||
Make sure any fa icons used in keys
|
|
||||||
or values are properly aligned.
|
|
||||||
*/
|
|
||||||
.fa {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
@ -1236,6 +1198,43 @@ button.with-padding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-list {
|
||||||
|
border: 0.1rem solid $gray1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.info-list-entry {
|
||||||
|
background: $list-entry-bg;
|
||||||
|
border: 0.1rem solid transparent;
|
||||||
|
padding: 0.25rem;
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
background: $list-entry-alternate-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max(20%, 10rem) 1fr;
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt, dd {
|
||||||
|
/*
|
||||||
|
Make sure any fa icons used in keys
|
||||||
|
or values are properly aligned.
|
||||||
|
*/
|
||||||
|
.fa {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.instance-rules {
|
.instance-rules {
|
||||||
list-style-position: inside;
|
list-style-position: inside;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -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) {
|
@media screen and (orientation: portrait) {
|
||||||
.reports .report .byline {
|
.reports .report .byline {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -36,6 +36,10 @@ import { useHasPermission } from "../../lib/navigation/util";
|
||||||
* - /settings/admin/actions
|
* - /settings/admin/actions
|
||||||
* - /settings/admin/actions/media
|
* - /settings/admin/actions/media
|
||||||
* - /settings/admin/actions/keys
|
* - /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() {
|
export default function AdminMenu() {
|
||||||
const permissions = ["admin"];
|
const permissions = ["admin"];
|
||||||
|
@ -54,6 +58,7 @@ export default function AdminMenu() {
|
||||||
<AdminInstanceMenu />
|
<AdminInstanceMenu />
|
||||||
<AdminEmojisMenu />
|
<AdminEmojisMenu />
|
||||||
<AdminActionsMenu />
|
<AdminActionsMenu />
|
||||||
|
<AdminHTTPHeaderPermissionsMenu />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -127,3 +132,25 @@ function AdminEmojisMenu() {
|
||||||
</MenuItem>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -29,15 +29,17 @@ import Keys from "./actions/keys";
|
||||||
import EmojiOverview from "./emoji/local/overview";
|
import EmojiOverview from "./emoji/local/overview";
|
||||||
import EmojiDetail from "./emoji/local/detail";
|
import EmojiDetail from "./emoji/local/detail";
|
||||||
import RemoteEmoji from "./emoji/remote";
|
import RemoteEmoji from "./emoji/remote";
|
||||||
|
import HeaderPermsOverview from "./http-header-permissions/overview";
|
||||||
|
import HeaderPermDetail from "./http-header-permissions/detail";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
EXPORTED COMPONENTS
|
EXPORTED COMPONENTS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - /settings/instance/settings
|
* - /settings/admin/instance/settings
|
||||||
* - /settings/instance/rules
|
* - /settings/admin/instance/rules
|
||||||
* - /settings/instance/rules/:ruleId
|
* - /settings/admin/instance/rules/:ruleId
|
||||||
* - /settings/admin/emojis
|
* - /settings/admin/emojis
|
||||||
* - /settings/admin/emojis/local
|
* - /settings/admin/emojis/local
|
||||||
* - /settings/admin/emojis/local/:emojiId
|
* - /settings/admin/emojis/local/:emojiId
|
||||||
|
@ -45,6 +47,10 @@ import RemoteEmoji from "./emoji/remote";
|
||||||
* - /settings/admin/actions
|
* - /settings/admin/actions
|
||||||
* - /settings/admin/actions/media
|
* - /settings/admin/actions/media
|
||||||
* - /settings/admin/actions/keys
|
* - /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() {
|
export default function AdminRouter() {
|
||||||
const parentUrl = useBaseUrl();
|
const parentUrl = useBaseUrl();
|
||||||
|
@ -57,6 +63,7 @@ export default function AdminRouter() {
|
||||||
<AdminInstanceRouter />
|
<AdminInstanceRouter />
|
||||||
<AdminEmojisRouter />
|
<AdminEmojisRouter />
|
||||||
<AdminActionsRouter />
|
<AdminActionsRouter />
|
||||||
|
<AdminHTTPHeaderPermissionsRouter />
|
||||||
</Router>
|
</Router>
|
||||||
</BaseUrlContext.Provider>
|
</BaseUrlContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -125,9 +132,9 @@ function AdminActionsRouter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - /settings/instance/settings
|
* - /settings/admin/instance/settings
|
||||||
* - /settings/instance/rules
|
* - /settings/admin/instance/rules
|
||||||
* - /settings/instance/rules/:ruleId
|
* - /settings/admin/instance/rules/:ruleId
|
||||||
*/
|
*/
|
||||||
function AdminInstanceRouter() {
|
function AdminInstanceRouter() {
|
||||||
const parentUrl = useBaseUrl();
|
const parentUrl = useBaseUrl();
|
||||||
|
@ -149,3 +156,29 @@ function AdminInstanceRouter() {
|
||||||
</BaseUrlContext.Provider>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ export default function AccountsPending() {
|
||||||
itemToEntry={itemToEntry}
|
itemToEntry={itemToEntry}
|
||||||
isError={searchRes.isError}
|
isError={searchRes.isError}
|
||||||
error={searchRes.error}
|
error={searchRes.error}
|
||||||
emptyMessage="No pending account sign-ups."
|
emptyMessage={<b>No pending account sign-ups.</b>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -166,7 +166,7 @@ export function AccountSearchForm() {
|
||||||
itemToEntry={itemToEntry}
|
itemToEntry={itemToEntry}
|
||||||
isError={searchRes.isError}
|
isError={searchRes.isError}
|
||||||
error={searchRes.error}
|
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}
|
prevNextLinks={searchRes.data?.links}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -34,10 +34,11 @@ import MutationButton from "../../../components/form/mutation-button";
|
||||||
|
|
||||||
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../../lib/query/admin/domain-permissions/get";
|
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../../lib/query/admin/domain-permissions/get";
|
||||||
import { useAddDomainAllowMutation, useAddDomainBlockMutation, useRemoveDomainAllowMutation, useRemoveDomainBlockMutation } from "../../../lib/query/admin/domain-permissions/update";
|
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 { NoArg } from "../../../lib/types/query";
|
||||||
import { Error } from "../../../components/error";
|
import { Error } from "../../../components/error";
|
||||||
import { useBaseUrl } from "../../../lib/navigation/util";
|
import { useBaseUrl } from "../../../lib/navigation/util";
|
||||||
|
import { PermType } from "../../../lib/types/perm";
|
||||||
|
|
||||||
export default function DomainPermDetail() {
|
export default function DomainPermDetail() {
|
||||||
const baseUrl = useBaseUrl();
|
const baseUrl = useBaseUrl();
|
||||||
|
|
|
@ -26,8 +26,9 @@ import { useTextInput } from "../../../lib/form";
|
||||||
import { TextInput } from "../../../components/form/inputs";
|
import { TextInput } from "../../../components/form/inputs";
|
||||||
import Loading from "../../../components/loading";
|
import Loading from "../../../components/loading";
|
||||||
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../../lib/query/admin/domain-permissions/get";
|
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 { NoArg } from "../../../lib/types/query";
|
||||||
|
import { PermType } from "../../../lib/types/perm";
|
||||||
|
|
||||||
export default function DomainPermissionsOverview() {
|
export default function DomainPermissionsOverview() {
|
||||||
// Parse perm type from routing params.
|
// Parse perm type from routing params.
|
||||||
|
|
Loading…
Reference in a new issue