From 637f188ebec71fe4b0b80bbab4592d4c269d7d93 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:46:06 +0200 Subject: [PATCH] [feature] Allow import/export/creation of domain allows via admin panel (#2264) * it's happening! * aaa * fix silly whoopsie * it's working pa! it's working ma! * model report parameters * shuffle some more stuff around * getting there * oo hoo * finish tidying up for now * aaa * fix use form submit errors * peepee poo poo * aaaaa * ffff * they see me typin', they hatin' * boop * aaa * oooo * typing typing tappa tappa * almost done typing * weee * alright * push it push it real good doo doo doo doo doo doo * thingy no worky * almost done * mutation modifers not quite right * hmm * it works * view blocks + allows nicely * it works! * typia install * the old linterino * linter plz --- .drone.yml | 3 +- .goreleaser.yml | 1 + CONTRIBUTING.md | 6 +- Dockerfile | 1 + internal/api/client/admin/domainpermission.go | 2 +- web/source/package.json | 9 +- web/source/settings/admin/accounts/detail.jsx | 6 +- .../admin/domain-permissions/detail.tsx | 254 ++++++++++ .../export-format-table.jsx | 0 .../form.jsx => domain-permissions/form.tsx} | 68 ++- .../domain-permissions/import-export.tsx | 90 ++++ .../admin/domain-permissions/index.tsx | 49 ++ .../admin/domain-permissions/overview.tsx | 198 ++++++++ .../process.tsx} | 179 ++++--- .../settings/admin/emoji/category-select.jsx | 5 +- .../settings/admin/emoji/local/detail.js | 18 +- .../settings/admin/emoji/local/new-emoji.js | 10 +- .../settings/admin/emoji/local/overview.js | 4 +- .../admin/emoji/local/use-shortcode.js | 8 +- .../settings/admin/emoji/remote/index.js | 4 +- .../admin/emoji/remote/parse-from-toot.js | 14 +- .../settings/admin/federation/detail.js | 168 ------- .../admin/federation/import-export/index.jsx | 75 --- .../settings/admin/federation/overview.js | 101 ---- web/source/settings/admin/reports/detail.jsx | 14 +- web/source/settings/admin/reports/index.jsx | 7 +- web/source/settings/admin/settings/index.jsx | 13 +- web/source/settings/admin/settings/rules.jsx | 4 +- .../components/authorization/index.tsx | 7 +- .../components/authorization/login.tsx | 15 +- .../{check-list.jsx => check-list.tsx} | 46 +- .../form/{inputs.jsx => inputs.tsx} | 106 +++-- .../settings/components/user-logout-card.jsx | 14 +- web/source/settings/index.js | 12 +- .../settings/lib/form/{bool.jsx => bool.tsx} | 17 +- .../form/{check-list.jsx => check-list.tsx} | 158 ++++--- .../lib/form/{combo-box.jsx => combo-box.tsx} | 23 +- .../form/{field-array.jsx => field-array.tsx} | 50 +- .../settings/lib/form/{file.jsx => file.tsx} | 92 ++-- ...{form-with-data.jsx => form-with-data.tsx} | 33 +- ...orm-mutations.js => get-form-mutations.ts} | 46 +- web/source/settings/lib/form/index.js | 83 ---- web/source/settings/lib/form/index.ts | 114 +++++ .../lib/form/{radio.jsx => radio.tsx} | 18 +- web/source/settings/lib/form/submit.js | 67 --- web/source/settings/lib/form/submit.ts | 140 ++++++ .../settings/lib/form/{text.jsx => text.tsx} | 51 +- web/source/settings/lib/form/types.ts | 264 +++++++++++ .../settings/lib/query/admin/custom-emoji.js | 194 -------- .../lib/query/admin/custom-emoji/index.ts | 307 ++++++++++++ .../query/admin/domain-permissions/export.ts | 155 ++++++ .../lib/query/admin/domain-permissions/get.ts | 56 +++ .../query/admin/domain-permissions/import.ts | 140 ++++++ .../query/admin/domain-permissions/process.ts | 163 +++++++ .../query/admin/domain-permissions/update.ts | 109 +++++ .../settings/lib/query/admin/import-export.js | 264 ----------- web/source/settings/lib/query/admin/index.js | 165 ------- web/source/settings/lib/query/admin/index.ts | 148 ++++++ .../settings/lib/query/admin/reports.js | 51 -- .../settings/lib/query/admin/reports/index.ts | 83 ++++ web/source/settings/lib/query/gts-api.ts | 16 +- web/source/settings/lib/query/lib.js | 81 ---- web/source/settings/lib/query/oauth/index.ts | 10 +- .../settings/lib/query/query-modifiers.ts | 150 ++++++ web/source/settings/lib/query/transforms.ts | 78 +++ web/source/settings/lib/query/user/index.ts | 8 +- .../index.js => lib/types/custom-emoji.ts} | 46 +- .../settings/lib/types/domain-permission.ts | 97 ++++ web/source/settings/lib/types/instance.ts | 91 ++++ web/source/settings/lib/types/query.ts | 95 ++++ web/source/settings/lib/types/report.ts | 144 ++++++ .../domain-permission.ts} | 37 +- web/source/settings/style.css | 14 +- web/source/settings/user/profile.js | 16 +- web/source/settings/user/settings.js | 4 +- web/source/tsconfig.json | 8 +- web/source/yarn.lock | 447 +++++++++++++++++- 77 files changed, 4154 insertions(+), 1690 deletions(-) create mode 100644 web/source/settings/admin/domain-permissions/detail.tsx rename web/source/settings/admin/{federation/import-export => domain-permissions}/export-format-table.jsx (100%) rename web/source/settings/admin/{federation/import-export/form.jsx => domain-permissions/form.tsx} (62%) create mode 100644 web/source/settings/admin/domain-permissions/import-export.tsx create mode 100644 web/source/settings/admin/domain-permissions/index.tsx create mode 100644 web/source/settings/admin/domain-permissions/overview.tsx rename web/source/settings/admin/{federation/import-export/process.jsx => domain-permissions/process.tsx} (61%) delete mode 100644 web/source/settings/admin/federation/detail.js delete mode 100644 web/source/settings/admin/federation/import-export/index.jsx delete mode 100644 web/source/settings/admin/federation/overview.js rename web/source/settings/components/{check-list.jsx => check-list.tsx} (68%) rename web/source/settings/components/form/{inputs.jsx => inputs.tsx} (52%) rename web/source/settings/lib/form/{bool.jsx => bool.tsx} (78%) rename web/source/settings/lib/form/{check-list.jsx => check-list.tsx} (53%) rename web/source/settings/lib/form/{combo-box.jsx => combo-box.tsx} (72%) rename web/source/settings/lib/form/{field-array.jsx => field-array.tsx} (57%) rename web/source/settings/lib/form/{file.jsx => file.tsx} (51%) rename web/source/settings/lib/form/{form-with-data.jsx => form-with-data.tsx} (55%) rename web/source/settings/lib/form/{get-form-mutations.js => get-form-mutations.ts} (52%) delete mode 100644 web/source/settings/lib/form/index.js create mode 100644 web/source/settings/lib/form/index.ts rename web/source/settings/lib/form/{radio.jsx => radio.tsx} (77%) delete mode 100644 web/source/settings/lib/form/submit.js create mode 100644 web/source/settings/lib/form/submit.ts rename web/source/settings/lib/form/{text.jsx => text.tsx} (64%) create mode 100644 web/source/settings/lib/form/types.ts delete mode 100644 web/source/settings/lib/query/admin/custom-emoji.js create mode 100644 web/source/settings/lib/query/admin/custom-emoji/index.ts create mode 100644 web/source/settings/lib/query/admin/domain-permissions/export.ts create mode 100644 web/source/settings/lib/query/admin/domain-permissions/get.ts create mode 100644 web/source/settings/lib/query/admin/domain-permissions/import.ts create mode 100644 web/source/settings/lib/query/admin/domain-permissions/process.ts create mode 100644 web/source/settings/lib/query/admin/domain-permissions/update.ts delete mode 100644 web/source/settings/lib/query/admin/import-export.js delete mode 100644 web/source/settings/lib/query/admin/index.js create mode 100644 web/source/settings/lib/query/admin/index.ts delete mode 100644 web/source/settings/lib/query/admin/reports.js create mode 100644 web/source/settings/lib/query/admin/reports/index.ts delete mode 100644 web/source/settings/lib/query/lib.js create mode 100644 web/source/settings/lib/query/query-modifiers.ts create mode 100644 web/source/settings/lib/query/transforms.ts rename web/source/settings/{admin/federation/index.js => lib/types/custom-emoji.ts} (58%) create mode 100644 web/source/settings/lib/types/domain-permission.ts create mode 100644 web/source/settings/lib/types/instance.ts create mode 100644 web/source/settings/lib/types/query.ts create mode 100644 web/source/settings/lib/types/report.ts rename web/source/settings/lib/{domain-block.js => util/domain-permission.ts} (54%) diff --git a/.drone.yml b/.drone.yml index c398db390..8e2aebb86 100644 --- a/.drone.yml +++ b/.drone.yml @@ -54,6 +54,7 @@ steps: path: /tmp/cache commands: - yarn --cwd ./web/source install --frozen-lockfile --cache-folder /tmp/cache + - yarn --cwd ./web/source ts-patch install # https://typia.io/docs/setup/#manual-setup - name: web-lint image: node:18-alpine @@ -191,6 +192,6 @@ steps: --- kind: signature -hmac: c3efbd528a76016562f88ae435141cfb5fd6d4d07b6ad2a24ecc23cb529cc1c6 +hmac: d7b93470276a0df7e4d862941489f00da107df3d085200009b776d33599e6043 ... diff --git a/.goreleaser.yml b/.goreleaser.yml index 1b49136c7..a49bb32e8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -8,6 +8,7 @@ before: - sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" web/assets/swagger.yaml # Install web deps + bundle web assets - yarn --cwd ./web/source install + - yarn --cwd ./web/source ts-patch install # https://typia.io/docs/setup/#manual-setup - yarn --cwd ./web/source build builds: # https://goreleaser.com/customization/build/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8218564d..628832e1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -229,13 +229,15 @@ Using [NVM](https://github.com/nvm-sh/nvm) is one convenient way to install them To install frontend dependencies: ```bash -yarn --cwd web/source +yarn --cwd ./web/source install && yarn --cwd ./web/source ts-patch install ``` +The `ts-patch` step is necessary because of Typia, which we use for some type validation: see [Typia install docs](https://typia.io/docs/setup/#manual-setup). + To recompile frontend bundles into `web/assets/dist`: ```bash -yarn --cwd web/source build +yarn --cwd ./web/source build ``` #### Live Reloading diff --git a/Dockerfile b/Dockerfile index d772f7497..7c1cce4d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ FROM --platform=${BUILDPLATFORM} node:18-alpine AS bundler COPY web web RUN yarn --cwd ./web/source install && \ + yarn --cwd ./web/source ts-patch install && \ yarn --cwd ./web/source build && \ rm -rf ./web/source diff --git a/internal/api/client/admin/domainpermission.go b/internal/api/client/admin/domainpermission.go index bd6b83425..203eddc8b 100644 --- a/internal/api/client/admin/domainpermission.go +++ b/internal/api/client/admin/domainpermission.go @@ -95,7 +95,7 @@ func (m *Module) createDomainPermissions( if importing && form.Domains.Size == 0 { err = errors.New("import was specified but list of domains is empty") - } else if form.Domain == "" { + } else if !importing && form.Domain == "" { err = errors.New("empty domain provided") } diff --git a/web/source/package.json b/web/source/package.json index d3c1cbe2b..20f525228 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -45,6 +45,10 @@ "@browserify/envify": "^6.0.0", "@browserify/uglifyify": "^6.0.0", "@joepie91/eslint-config": "^1.1.1", + "@types/bluebird": "^3.5.39", + "@types/is-valid-domain": "^0.0.2", + "@types/papaparse": "^5.3.9", + "@types/psl": "^1.1.1", "@types/react-dom": "^18.2.8", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", @@ -63,7 +67,10 @@ "postcss-nested": "^6.0.0", "source-map-loader": "^4.0.1", "ts-loader": "^9.4.4", + "ts-node": "^10.9.1", + "ts-patch": "^3.0.2", "tsify": "^5.0.4", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "typia": "^5.1.6" } } diff --git a/web/source/settings/admin/accounts/detail.jsx b/web/source/settings/admin/accounts/detail.jsx index 0e906cd1c..63049c149 100644 --- a/web/source/settings/admin/accounts/detail.jsx +++ b/web/source/settings/admin/accounts/detail.jsx @@ -22,13 +22,13 @@ const { useRoute, Redirect } = require("wouter"); const query = require("../../lib/query"); -const FormWithData = require("../../lib/form/form-with-data"); +const FormWithData = require("../../lib/form/form-with-data").default; const { useBaseUrl } = require("../../lib/navigation/util"); const FakeProfile = require("../../components/fake-profile"); const MutationButton = require("../../components/form/mutation-button"); -const useFormSubmit = require("../../lib/form/submit"); +const useFormSubmit = require("../../lib/form/submit").default; const { useValue, useTextInput } = require("../../lib/form"); const { TextInput } = require("../../components/form/inputs"); @@ -77,7 +77,7 @@ function AccountDetailForm({ data: account }) { function ModifyAccount({ account }) { const form = { id: useValue("id", account.id), - reason: useTextInput("text", {}) + reason: useTextInput("text") }; const [modifyAccount, result] = useFormSubmit(form, query.useActionAccountMutation()); diff --git a/web/source/settings/admin/domain-permissions/detail.tsx b/web/source/settings/admin/domain-permissions/detail.tsx new file mode 100644 index 000000000..f74802666 --- /dev/null +++ b/web/source/settings/admin/domain-permissions/detail.tsx @@ -0,0 +1,254 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import React from "react"; + +import { useMemo } from "react"; +import { useLocation } from "wouter"; + +import { useTextInput, useBoolInput } from "../../lib/form"; + +import useFormSubmit from "../../lib/form/submit"; + +import { TextInput, Checkbox, TextArea } from "../../components/form/inputs"; + +import Loading from "../../components/loading"; +import BackButton from "../../components/back-button"; +import MutationButton from "../../components/form/mutation-button"; + +import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get"; +import { useAddDomainAllowMutation, useAddDomainBlockMutation, useRemoveDomainAllowMutation, useRemoveDomainBlockMutation } from "../../lib/query/admin/domain-permissions/update"; +import { DomainPerm, PermType } from "../../lib/types/domain-permission"; +import { NoArg } from "../../lib/types/query"; +import { Error } from "../../components/error"; + +export interface DomainPermDetailProps { + baseUrl: string; + permType: PermType; + domain: string; +} + +export default function DomainPermDetail({ baseUrl, permType, domain }: DomainPermDetailProps) { + const { data: domainBlocks = {}, isLoading: isLoadingDomainBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" }); + const { data: domainAllows = {}, isLoading: isLoadingDomainAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" }); + + let isLoading; + switch (permType) { + case "block": + isLoading = isLoadingDomainBlocks; + break; + case "allow": + isLoading = isLoadingDomainAllows; + break; + default: + throw "perm type unknown"; + } + + if (domain == "view") { + // Retrieve domain from form field submission. + domain = (new URL(document.location.toString())).searchParams.get("domain")?? "unknown"; + } + + if (domain == "unknown") { + throw "unknown domain"; + } + + // Normalize / decode domain (it may be URL-encoded). + domain = decodeURIComponent(domain); + + // Check if we already have a perm of the desired type for this domain. + const existingPerm: DomainPerm | undefined = useMemo(() => { + if (permType == "block") { + return domainBlocks[domain]; + } else { + return domainAllows[domain]; + } + }, [domainBlocks, domainAllows, domain, permType]); + + let infoContent: React.JSX.Element; + + if (isLoading) { + infoContent = ; + } else if (existingPerm == undefined) { + infoContent = No stored {permType} yet, you can add one below:; + } else { + infoContent = ( +
+ + Editing domain permissions isn't implemented yet, check here for progress +
+ ); + } + + return ( +
+

Domain {permType} for: {domain}

+ {infoContent} + +
+ ); +} + +interface DomainPermFormProps { + defaultDomain: string; + perm?: DomainPerm; + permType: PermType; + baseUrl: string; +} + +function DomainPermForm({ defaultDomain, perm, permType, baseUrl }: DomainPermFormProps) { + const isExistingPerm = perm !== undefined; + const disabledForm = isExistingPerm + ? { + disabled: true, + title: "Domain permissions currently cannot be edited." + } + : { + disabled: false, + title: "", + }; + + const form = { + domain: useTextInput("domain", { source: perm, defaultValue: defaultDomain }), + obfuscate: useBoolInput("obfuscate", { source: perm }), + commentPrivate: useTextInput("private_comment", { source: perm }), + commentPublic: useTextInput("public_comment", { source: perm }) + }; + + // Check which perm type we're meant to be handling + // here, and use appropriate mutations and results. + // We can't call these hooks conditionally because + // react is like "weh" (mood), but we can decide + // which ones to use conditionally. + const [ addBlock, addBlockResult ] = useAddDomainBlockMutation(); + const [ removeBlock, removeBlockResult] = useRemoveDomainBlockMutation({ fixedCacheKey: perm?.id }); + const [ addAllow, addAllowResult ] = useAddDomainAllowMutation(); + const [ removeAllow, removeAllowResult ] = useRemoveDomainAllowMutation({ fixedCacheKey: perm?.id }); + + const [ + addTrigger, + addResult, + removeTrigger, + removeResult, + ] = useMemo(() => { + return permType == "block" + ? [ + addBlock, + addBlockResult, + removeBlock, + removeBlockResult, + ] + : [ + addAllow, + addAllowResult, + removeAllow, + removeAllowResult, + ]; + }, [permType, + addBlock, addBlockResult, removeBlock, removeBlockResult, + addAllow, addAllowResult, removeAllow, removeAllowResult, + ]); + + // Use appropriate submission params for this permType. + const [submitForm, submitFormResult] = useFormSubmit(form, [addTrigger, addResult], { changedOnly: false }); + + // Uppercase first letter of given permType. + const permTypeUpper = useMemo(() => { + return permType.charAt(0).toUpperCase() + permType.slice(1); + }, [permType]); + + const [location, setLocation] = useLocation(); + + function verifyUrlThenSubmit(e) { + // Adding a new domain permissions happens on a url like + // "/settings/admin/domain-permissions/:permType/domain.com", + // but if domain input changes, that doesn't match anymore + // and causes issues later on so, before submitting the form, + // silently change url, and THEN submit. + let correctUrl = `${baseUrl}/${form.domain.value}`; + if (location != correctUrl) { + setLocation(correctUrl); + } + return submitForm(e); + } + + return ( +
+ + + + +