diff options
Diffstat (limited to 'web/source/settings/lib')
9 files changed, 511 insertions, 19 deletions
diff --git a/web/source/settings/lib/navigation/menu.tsx b/web/source/settings/lib/navigation/menu.tsx index 514e3ea2f..2bd07a055 100644 --- a/web/source/settings/lib/navigation/menu.tsx +++ b/web/source/settings/lib/navigation/menu.tsx @@ -110,12 +110,19 @@ export function MenuItem(props: PropsWithChildren<MenuItemProps>) { if (topLevel) { classNames.push("category", "top-level"); } else { - if (thisLevel === 1 && hasChildren) { - classNames.push("category", "expanding"); - } else if (thisLevel === 1 && !hasChildren) { - classNames.push("view", "expanding"); - } else if (thisLevel === 2) { - classNames.push("view", "nested"); + switch (true) { + case thisLevel === 1 && hasChildren: + classNames.push("category", "expanding"); + break; + case thisLevel === 1 && !hasChildren: + classNames.push("view", "expanding"); + break; + case thisLevel >= 2 && hasChildren: + classNames.push("nested", "category"); + break; + case thisLevel >= 2 && !hasChildren: + classNames.push("nested", "view"); + break; } } diff --git a/web/source/settings/lib/query/admin/domain-permissions/drafts.ts b/web/source/settings/lib/query/admin/domain-permissions/drafts.ts new file mode 100644 index 000000000..1a85f9dde --- /dev/null +++ b/web/source/settings/lib/query/admin/domain-permissions/drafts.ts @@ -0,0 +1,173 @@ +/* + 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 type { + DomainPerm, + DomainPermDraftCreateParams, + DomainPermDraftSearchParams, + DomainPermDraftSearchResp, +} from "../../../types/domain-permission"; +import parse from "parse-link-header"; +import { PermType } from "../../../types/perm"; + +const extended = gtsApi.injectEndpoints({ + endpoints: (build) => ({ + searchDomainPermissionDrafts: build.query<DomainPermDraftSearchResp, DomainPermDraftSearchParams>({ + query: (form) => { + const params = new(URLSearchParams); + Object.entries(form).forEach(([k, v]) => { + if (v !== undefined) { + params.append(k, v); + } + }); + + let query = ""; + if (params.size !== 0) { + query = `?${params.toString()}`; + } + + return { + url: `/api/v1/admin/domain_permission_drafts${query}` + }; + }, + // Headers required for paging. + transformResponse: (apiResp: DomainPerm[], meta) => { + const drafts = apiResp; + const linksStr = meta?.response?.headers.get("Link"); + const links = parse(linksStr); + return { drafts, links }; + }, + // Only provide TRANSFORMED tag id since this model is not the same + // as getDomainPermissionDraft model (due to transformResponse). + providesTags: [{ type: "DomainPermissionDraft", id: "TRANSFORMED" }] + }), + + getDomainPermissionDraft: build.query<DomainPerm, string>({ + query: (id) => ({ + url: `/api/v1/admin/domain_permission_drafts/${id}` + }), + providesTags: (_result, _error, id) => [ + { type: 'DomainPermissionDraft', id } + ], + }), + + createDomainPermissionDraft: build.mutation<DomainPerm, DomainPermDraftCreateParams>({ + query: (formData) => ({ + method: "POST", + url: `/api/v1/admin/domain_permission_drafts`, + asForm: true, + body: formData, + discardEmpty: true + }), + invalidatesTags: [{ type: "DomainPermissionDraft", id: "TRANSFORMED" }], + }), + + acceptDomainPermissionDraft: build.mutation<DomainPerm, { id: string, overwrite?: boolean, permType: PermType }>({ + query: ({ id, overwrite }) => ({ + method: "POST", + url: `/api/v1/admin/domain_permission_drafts/${id}/accept`, + asForm: true, + body: { + overwrite: overwrite, + }, + discardEmpty: true + }), + invalidatesTags: (res, _error, { id, permType }) => { + const invalidated: any[] = []; + + // If error, nothing to invalidate. + if (!res) { + return invalidated; + } + + // Invalidate this draft by ID, and + // the transformed list of all drafts. + invalidated.push( + { type: 'DomainPermissionDraft', id: id }, + { type: "DomainPermissionDraft", id: "TRANSFORMED" }, + ); + + // Invalidate cached blocks/allows depending + // on the permType of the accepted draft. + if (permType === "allow") { + invalidated.push("domainAllows"); + } else { + invalidated.push("domainBlocks"); + } + + return invalidated; + } + }), + + removeDomainPermissionDraft: build.mutation<DomainPerm, { id: string, exclude_target?: boolean }>({ + query: ({ id, exclude_target }) => ({ + method: "POST", + url: `/api/v1/admin/domain_permission_drafts/${id}/remove`, + asForm: true, + body: { + exclude_target: exclude_target, + }, + discardEmpty: true + }), + invalidatesTags: (res, _error, { id }) => + res + ? [ + { type: "DomainPermissionDraft", id }, + { type: "DomainPermissionDraft", id: "TRANSFORMED" }, + ] + : [], + }) + + }), +}); + +/** + * View domain permission drafts. + */ +const useLazySearchDomainPermissionDraftsQuery = extended.useLazySearchDomainPermissionDraftsQuery; + +/** + * Get domain permission draft with the given ID. + */ +const useGetDomainPermissionDraftQuery = extended.useGetDomainPermissionDraftQuery; + +/** + * Create a domain permission draft with the given parameters. + */ +const useCreateDomainPermissionDraftMutation = extended.useCreateDomainPermissionDraftMutation; + +/** + * Accept a domain permission draft, turning it into an enforced domain permission. + */ +const useAcceptDomainPermissionDraftMutation = extended.useAcceptDomainPermissionDraftMutation; + +/** + * Remove a domain permission draft, optionally ignoring all future drafts targeting the given domain. + */ +const useRemoveDomainPermissionDraftMutation = extended.useRemoveDomainPermissionDraftMutation; + +export { + useLazySearchDomainPermissionDraftsQuery, + useGetDomainPermissionDraftQuery, + useCreateDomainPermissionDraftMutation, + useAcceptDomainPermissionDraftMutation, + useRemoveDomainPermissionDraftMutation, +}; diff --git a/web/source/settings/lib/query/admin/domain-permissions/excludes.ts b/web/source/settings/lib/query/admin/domain-permissions/excludes.ts new file mode 100644 index 000000000..6b8f16cad --- /dev/null +++ b/web/source/settings/lib/query/admin/domain-permissions/excludes.ts @@ -0,0 +1,124 @@ +/* + 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 type { + DomainPerm, + DomainPermExcludeCreateParams, + DomainPermExcludeSearchParams, + DomainPermExcludeSearchResp, +} from "../../../types/domain-permission"; +import parse from "parse-link-header"; + +const extended = gtsApi.injectEndpoints({ + endpoints: (build) => ({ + searchDomainPermissionExcludes: build.query<DomainPermExcludeSearchResp, DomainPermExcludeSearchParams>({ + query: (form) => { + const params = new(URLSearchParams); + Object.entries(form).forEach(([k, v]) => { + if (v !== undefined) { + params.append(k, v); + } + }); + + let query = ""; + if (params.size !== 0) { + query = `?${params.toString()}`; + } + + return { + url: `/api/v1/admin/domain_permission_excludes${query}` + }; + }, + // Headers required for paging. + transformResponse: (apiResp: DomainPerm[], meta) => { + const excludes = apiResp; + const linksStr = meta?.response?.headers.get("Link"); + const links = parse(linksStr); + return { excludes, links }; + }, + // Only provide TRANSFORMED tag id since this model is not the same + // as getDomainPermissionExclude model (due to transformResponse). + providesTags: [{ type: "DomainPermissionExclude", id: "TRANSFORMED" }] + }), + + getDomainPermissionExclude: build.query<DomainPerm, string>({ + query: (id) => ({ + url: `/api/v1/admin/domain_permission_excludes/${id}` + }), + providesTags: (_result, _error, id) => [ + { type: 'DomainPermissionExclude', id } + ], + }), + + createDomainPermissionExclude: build.mutation<DomainPerm, DomainPermExcludeCreateParams>({ + query: (formData) => ({ + method: "POST", + url: `/api/v1/admin/domain_permission_excludes`, + asForm: true, + body: formData, + discardEmpty: true + }), + invalidatesTags: [{ type: "DomainPermissionExclude", id: "TRANSFORMED" }], + }), + + deleteDomainPermissionExclude: build.mutation<DomainPerm, string>({ + query: (id) => ({ + method: "DELETE", + url: `/api/v1/admin/domain_permission_excludes/${id}`, + }), + invalidatesTags: (res, _error, id) => + res + ? [ + { type: "DomainPermissionExclude", id }, + { type: "DomainPermissionExclude", id: "TRANSFORMED" }, + ] + : [], + }) + + }), +}); + +/** + * View domain permission excludes. + */ +const useLazySearchDomainPermissionExcludesQuery = extended.useLazySearchDomainPermissionExcludesQuery; + +/** + * Get domain permission exclude with the given ID. + */ +const useGetDomainPermissionExcludeQuery = extended.useGetDomainPermissionExcludeQuery; + +/** + * Create a domain permission exclude with the given parameters. + */ +const useCreateDomainPermissionExcludeMutation = extended.useCreateDomainPermissionExcludeMutation; + +/** + * Delete a domain permission exclude. + */ +const useDeleteDomainPermissionExcludeMutation = extended.useDeleteDomainPermissionExcludeMutation; + +export { + useLazySearchDomainPermissionExcludesQuery, + useGetDomainPermissionExcludeQuery, + useCreateDomainPermissionExcludeMutation, + useDeleteDomainPermissionExcludeMutation, +}; diff --git a/web/source/settings/lib/query/admin/domain-permissions/get.ts b/web/source/settings/lib/query/admin/domain-permissions/get.ts index 3e27742d4..ae7ac7960 100644 --- a/web/source/settings/lib/query/admin/domain-permissions/get.ts +++ b/web/source/settings/lib/query/admin/domain-permissions/get.ts @@ -37,6 +37,12 @@ const extended = gtsApi.injectEndpoints({ }), transformResponse: listToKeyedObject<DomainPerm>("domain"), }), + + domainPermissionDrafts: build.query<any, void>({ + query: () => ({ + url: `/api/v1/admin/domain_permission_drafts` + }), + }), }), }); diff --git a/web/source/settings/lib/query/admin/domain-permissions/import.ts b/web/source/settings/lib/query/admin/domain-permissions/import.ts index dde488625..cbcf44964 100644 --- a/web/source/settings/lib/query/admin/domain-permissions/import.ts +++ b/web/source/settings/lib/query/admin/domain-permissions/import.ts @@ -24,7 +24,7 @@ import { type DomainPerm, type ImportDomainPermsParams, type MappedDomainPerms, - isDomainPermInternalKey, + stripOnImport, } from "../../../types/domain-permission"; import { listToKeyedObject } from "../../transforms"; @@ -83,7 +83,7 @@ function importEntriesProcessor(formData: ImportDomainPermsParams): (_entry: Dom // Unset all internal processing keys // and any undefined keys on this entry. Object.entries(entry).forEach(([key, val]: [keyof DomainPerm, any]) => { - if (val == undefined || isDomainPermInternalKey(key)) { + if (val == undefined || stripOnImport(key)) { delete entry[key]; } }); diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts index 911ea58c7..9543819a9 100644 --- a/web/source/settings/lib/query/gts-api.ts +++ b/web/source/settings/lib/query/gts-api.ts @@ -169,6 +169,8 @@ export const gtsApi = createApi({ "HTTPHeaderBlocks", "DefaultInteractionPolicies", "InteractionRequest", + "DomainPermissionDraft", + "DomainPermissionExclude" ], 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 ccf7c9c57..1a0a9bd0b 100644 --- a/web/source/settings/lib/types/domain-permission.ts +++ b/web/source/settings/lib/types/domain-permission.ts @@ -19,11 +19,12 @@ import typia from "typia"; import { PermType } from "./perm"; +import { Links } from "parse-link-header"; export const validateDomainPerms = typia.createValidate<DomainPerm[]>(); /** - * A single domain permission entry (block or allow). + * A single domain permission entry (block, allow, draft, ignore). */ export interface DomainPerm { id?: string; @@ -32,11 +33,14 @@ export interface DomainPerm { private_comment?: string; public_comment?: string; created_at?: string; + created_by?: string; + subscription_id?: string; - // Internal processing keys; remove - // before serdes of domain perm. + // Keys that should be stripped before + // sending the domain permission (if imported). + + permission_type?: PermType; key?: string; - permType?: PermType; suggest?: string; valid?: boolean; checked?: boolean; @@ -53,9 +57,9 @@ export interface MappedDomainPerms { [key: string]: DomainPerm; } -const domainPermInternalKeys: Set<keyof DomainPerm> = new Set([ +const domainPermStripOnImport: Set<keyof DomainPerm> = new Set([ "key", - "permType", + "permission_type", "suggest", "valid", "checked", @@ -65,15 +69,14 @@ const domainPermInternalKeys: Set<keyof DomainPerm> = new Set([ ]); /** - * Returns true if provided DomainPerm Object key is - * "internal"; ie., it's just for our use, and it shouldn't - * be serialized to or deserialized from the GtS API. + * Returns true if provided DomainPerm Object key is one + * that should be stripped when importing a domain permission. * * @param key * @returns */ -export function isDomainPermInternalKey(key: keyof DomainPerm) { - return domainPermInternalKeys.has(key); +export function stripOnImport(key: keyof DomainPerm) { + return domainPermStripOnImport.has(key); } export interface ImportDomainPermsParams { @@ -94,3 +97,119 @@ export interface ExportDomainPermsParams { action: "export" | "export-file"; exportType: "json" | "csv" | "plain"; } + +/** + * Parameters for GET to /api/v1/admin/domain_permission_drafts. + */ +export interface DomainPermDraftSearchParams { + /** + * Show only drafts created by the given subscription ID. + */ + subscription_id?: string; + /** + * Return only drafts that target the given domain. + */ + domain?: string; + /** + * Filter on "block" or "allow" type drafts. + */ + permission_type?: PermType; + /** + * Return only items *OLDER* than the given max ID (for paging downwards). + * The item with the specified ID will not be included in the response. + */ + max_id?: string; + /** + * Return only items *NEWER* than the given since ID. + * The item with the specified ID will not be included in the response. + */ + since_id?: string; + /** + * Return only items immediately *NEWER* than the given min ID (for paging upwards). + * The item with the specified ID will not be included in the response. + */ + min_id?: string; + /** + * Number of items to return. + */ + limit?: number; +} + +export interface DomainPermDraftSearchResp { + drafts: DomainPerm[]; + links: Links | null; +} + +export interface DomainPermDraftCreateParams { + /** + * Domain to create the permission draft for. + */ + domain: string; + /** + * Create a draft "allow" or a draft "block". + */ + permission_type: PermType; + /** + * Obfuscate the name of the domain when serving it publicly. + * Eg., `example.org` becomes something like `ex***e.org`. + */ + obfuscate?: boolean; + /** + * Public comment about this domain permission. This will be displayed + * alongside the domain permission if you choose to share permissions. + */ + public_comment?: string; + /** + * Private comment about this domain permission. + * Will only be shown to other admins, so this is a useful way of + * internally keeping track of why a certain domain ended up permissioned. + */ + private_comment?: string; +} + +/** + * Parameters for GET to /api/v1/admin/domain_permission_excludes. + */ +export interface DomainPermExcludeSearchParams { + /** + * Return only excludes that target the given domain. + */ + domain?: string; + /** + * Return only items *OLDER* than the given max ID (for paging downwards). + * The item with the specified ID will not be included in the response. + */ + max_id?: string; + /** + * Return only items *NEWER* than the given since ID. + * The item with the specified ID will not be included in the response. + */ + since_id?: string; + /** + * Return only items immediately *NEWER* than the given min ID (for paging upwards). + * The item with the specified ID will not be included in the response. + */ + min_id?: string; + /** + * Number of items to return. + */ + limit?: number; +} + +export interface DomainPermExcludeSearchResp { + excludes: DomainPerm[]; + links: Links | null; +} + +export interface DomainPermExcludeCreateParams { + /** + * Domain to create the permission exclude for. + */ + domain: string; + /** + * Private comment about this domain permission. + * Will only be shown to other admins, so this is a useful way of + * internally keeping track of why a certain domain ended up permissioned. + */ + private_comment?: string; +} diff --git a/web/source/settings/lib/util/formvalidators.ts b/web/source/settings/lib/util/formvalidators.ts new file mode 100644 index 000000000..c509cf59d --- /dev/null +++ b/web/source/settings/lib/util/formvalidators.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/>. +*/ + +import isValidDomain from "is-valid-domain"; + +/** + * Validate the "domain" field of a form. + * @param domain + * @returns + */ +export function formDomainValidator(domain: string): string { + if (domain.length === 0) { + return ""; + } + + if (domain[domain.length-1] === ".") { + return "invalid domain"; + } + + const valid = isValidDomain(domain, { + subdomain: true, + wildcard: false, + allowUnicode: true, + topLevel: false, + }); + + if (valid) { + return ""; + } + + return "invalid domain"; +} diff --git a/web/source/settings/lib/util/index.ts b/web/source/settings/lib/util/index.ts index d016f3398..4c8a90626 100644 --- a/web/source/settings/lib/util/index.ts +++ b/web/source/settings/lib/util/index.ts @@ -41,3 +41,16 @@ export function UseOurInstanceAccount(account: AdminAccount): boolean { return !account.domain && account.username == ourDomain; } + +/** + * Uppercase first letter of given string. + */ +export function useCapitalize(i?: string): string { + return useMemo(() => { + if (i === undefined) { + return ""; + } + + return i.charAt(0).toUpperCase() + i.slice(1); + }, [i]); +} |