diff options
author | 2023-01-18 14:45:14 +0100 | |
---|---|---|
committer | 2023-01-18 14:45:14 +0100 | |
commit | 9b139b632098e6741b10fa87ff6224dcb5045947 (patch) | |
tree | c72b5c666ed01db7d1a18e531e5e01e07f504a46 /web/source/settings/lib/query | |
parent | [chore] Change default sqlite busy timeout to 5m (#1352) (diff) | |
download | gotosocial-9b139b632098e6741b10fa87ff6224dcb5045947.tar.xz |
[frogend] Settings refactor (#1318)
* yakshave new form field structure
* fully refactor user profile settings form
* use rtk query api for profile settings
* refactor user post settings
* refactor password change form
* refactor admin settings
* FormWithData structure for user forms
* admin actions refactor
* whitespace
* fix user settings data prop
* remove superfluous logging
* cleanup old code
* refactor federation/suspend (overview, detail)
* mostly abstracted (emoji) checkbox list
* refactor parse-from-toot
* refactor custom-emoji, progress on federation bulk
* loading icon styling to prevent big spinny
* refactor federation import-export interface
* cleanup old files
* [chore] Update/add license headers for 2023
* redux fixes
* text-field exports
* appease the linter
* refactor authentication with RTK Query
* fix login/logout state transition weirdness
* fixes/cleanup
* small linter-related fixes
* add eslint license header check, fix existing files
* remove old code, clarify comment
* clarify suspend on subdomains
* collapse if/else
* fa-fw width info comment
Diffstat (limited to 'web/source/settings/lib/query')
-rw-r--r-- | web/source/settings/lib/query/admin/custom-emoji.js | 195 | ||||
-rw-r--r-- | web/source/settings/lib/query/admin/import-export.js | 212 | ||||
-rw-r--r-- | web/source/settings/lib/query/admin/index.js | 84 | ||||
-rw-r--r-- | web/source/settings/lib/query/base.js | 60 | ||||
-rw-r--r-- | web/source/settings/lib/query/custom-emoji.js | 180 | ||||
-rw-r--r-- | web/source/settings/lib/query/index.js | 28 | ||||
-rw-r--r-- | web/source/settings/lib/query/lib.js | 75 | ||||
-rw-r--r-- | web/source/settings/lib/query/oauth.js | 158 | ||||
-rw-r--r-- | web/source/settings/lib/query/user.js | 44 |
9 files changed, 827 insertions, 209 deletions
diff --git a/web/source/settings/lib/query/admin/custom-emoji.js b/web/source/settings/lib/query/admin/custom-emoji.js new file mode 100644 index 000000000..94917f382 --- /dev/null +++ b/web/source/settings/lib/query/admin/custom-emoji.js @@ -0,0 +1,195 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); + +const { unwrapRes } = require("../lib"); + +module.exports = (build) => ({ + getAllEmoji: build.query({ + query: (params = {}) => ({ + url: "/api/v1/admin/custom_emojis", + params: { + limit: 0, + ...params + } + }), + providesTags: (res) => + res + ? [...res.map((emoji) => ({ type: "Emojis", id: emoji.id })), { type: "Emojis", id: "LIST" }] + : [{ type: "Emojis", id: "LIST" }] + }), + + getEmoji: build.query({ + query: (id) => ({ + url: `/api/v1/admin/custom_emojis/${id}` + }), + providesTags: (res, error, id) => [{ type: "Emojis", id }] + }), + + addEmoji: build.mutation({ + query: (form) => { + return { + method: "POST", + url: `/api/v1/admin/custom_emojis`, + asForm: true, + body: form, + discardEmpty: true + }; + }, + invalidatesTags: (res) => + res + ? [{ type: "Emojis", id: "LIST" }, { type: "Emojis", id: res.id }] + : [{ type: "Emojis", id: "LIST" }] + }), + + editEmoji: build.mutation({ + query: ({ id, ...patch }) => { + return { + method: "PATCH", + url: `/api/v1/admin/custom_emojis/${id}`, + asForm: true, + body: { + type: "modify", + ...patch + } + }; + }, + invalidatesTags: (res) => + res + ? [{ type: "Emojis", id: "LIST" }, { type: "Emojis", id: res.id }] + : [{ type: "Emojis", id: "LIST" }] + }), + + deleteEmoji: build.mutation({ + query: (id) => ({ + method: "DELETE", + url: `/api/v1/admin/custom_emojis/${id}` + }), + invalidatesTags: (res, error, id) => [{ type: "Emojis", id }] + }), + + searchStatusForEmoji: build.mutation({ + queryFn: (url, api, _extraOpts, baseQuery) => { + return Promise.try(() => { + return baseQuery({ + url: `/api/v2/search?q=${encodeURIComponent(url)}&resolve=true&limit=1` + }).then(unwrapRes); + }).then((searchRes) => { + return emojiFromSearchResult(searchRes); + }).then(({ type, domain, list }) => { + const state = api.getState(); + if (domain == new URL(state.oauth.instance).host) { + throw "LOCAL_INSTANCE"; + } + + // search for every mentioned emoji with the admin api to get their ID + return Promise.map(list, (emoji) => { + return baseQuery({ + url: `/api/v1/admin/custom_emojis`, + params: { + filter: `domain:${domain},shortcode:${emoji.shortcode}`, + limit: 1 + } + }).then((unwrapRes)).then((list) => list[0]); + }, { concurrency: 5 }).then((listWithIDs) => { + return { + data: { + type, + domain, + list: listWithIDs + } + }; + }); + }).catch((e) => { + return { error: e }; + }); + } + }), + + patchRemoteEmojis: build.mutation({ + queryFn: ({ action, ...formData }, _api, _extraOpts, baseQuery) => { + const data = []; + const errors = []; + + return Promise.each(formData.selectedEmoji, (emoji) => { + return Promise.try(() => { + let body = { + type: action + }; + + if (action == "copy") { + body.shortcode = emoji.shortcode; + if (formData.category.trim().length != 0) { + body.category = formData.category; + } + } + + return baseQuery({ + method: "PATCH", + url: `/api/v1/admin/custom_emojis/${emoji.id}`, + asForm: true, + body: body + }).then(unwrapRes); + }).then((res) => { + data.push([emoji.shortcode, res]); + }).catch((e) => { + let msg = e.message ?? e; + if (e.data.error) { + msg = e.data.error; + } + errors.push([emoji.shortcode, msg]); + }); + }).then(() => { + if (errors.length == 0) { + return { data }; + } else { + return { + error: errors + }; + } + }); + }, + invalidatesTags: () => [{ type: "Emojis", id: "LIST" }] + }) +}); + +function emojiFromSearchResult(searchRes) { + /* Parses the search response, prioritizing a toot result, + and returns referenced custom emoji + */ + let type; + + if (searchRes.statuses.length > 0) { + type = "statuses"; + } else if (searchRes.accounts.length > 0) { + type = "accounts"; + } else { + throw "NONE_FOUND"; + } + + let data = searchRes[type][0]; + + return { + type, + domain: (new URL(data.url)).host, // to get WEB_DOMAIN, see https://github.com/superseriousbusiness/gotosocial/issues/1225 + list: data.emojis + }; +}
\ No newline at end of file diff --git a/web/source/settings/lib/query/admin/import-export.js b/web/source/settings/lib/query/admin/import-export.js new file mode 100644 index 000000000..94e462bd2 --- /dev/null +++ b/web/source/settings/lib/query/admin/import-export.js @@ -0,0 +1,212 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); +const isValidDomain = require("is-valid-domain"); +const fileDownload = require("js-file-download"); + +const { + replaceCacheOnMutation, + domainListToObject, + unwrapRes +} = require("../lib"); + +function parseDomainList(list) { + if (list[0] == "[") { + return JSON.parse(list); + } else { + return list.split("\n").map((line) => { + let domain = line.trim(); + let valid = true; + if (domain.startsWith("http")) { + try { + domain = new URL(domain).hostname; + } catch (e) { + valid = false; + } + } + return domain.length > 0 + ? { domain, valid } + : null; + }).filter((a) => a); // not `null` + } +} + +function validateDomainList(list) { + list.forEach((entry) => { + entry.valid = (entry.valid !== false) && isValidDomain(entry.domain, { wildcard: true, allowUnicode: true }); + entry.checked = entry.valid; + }); + + return list; +} + +function deduplicateDomainList(list) { + let domains = new Set(); + return list.filter((entry) => { + if (domains.has(entry.domain)) { + return false; + } else { + domains.add(entry.domain); + return true; + } + }); +} + +module.exports = (build) => ({ + processDomainList: build.mutation({ + queryFn: (formData) => { + return Promise.try(() => { + if (formData.domains == undefined || formData.domains.length == 0) { + throw "No domains entered"; + } + return parseDomainList(formData.domains); + }).then((parsed) => { + return deduplicateDomainList(parsed); + }).then((deduped) => { + return validateDomainList(deduped); + }).then((data) => { + return { data }; + }).catch((e) => { + return { error: e.toString() }; + }); + } + }), + exportDomainList: build.mutation({ + queryFn: (formData, api, _extraOpts, baseQuery) => { + return Promise.try(() => { + return baseQuery({ + url: `/api/v1/admin/domain_blocks` + }); + }).then(unwrapRes).then((blockedInstances) => { + return blockedInstances.map((entry) => { + if (formData.exportType == "json") { + return { + domain: entry.domain, + public_comment: entry.public_comment + }; + } else { + return entry.domain; + } + }); + }).then((exportList) => { + if (formData.exportType == "json") { + return JSON.stringify(exportList); + } else { + return exportList.join("\n"); + } + }).then((exportAsString) => { + if (formData.action == "export") { + return { + data: exportAsString + }; + } else if (formData.action == "export-file") { + let domain = new URL(api.getState().oauth.instance).host; + let date = new Date(); + let mime; + + let filename = [ + domain, + "blocklist", + date.getFullYear(), + (date.getMonth() + 1).toString().padStart(2, "0"), + date.getDate().toString().padStart(2, "0"), + ].join("-"); + + if (formData.exportType == "json") { + filename += ".json"; + mime = "application/json"; + } else { + filename += ".txt"; + mime = "text/plain"; + } + + fileDownload(exportAsString, filename, mime); + } + return { data: null }; + }).catch((e) => { + return { error: e }; + }); + } + }), + importDomainList: build.mutation({ + query: (formData) => { + const { domains } = formData; + + // add/replace comments, obfuscation data + let process = entryProcessor(formData); + domains.forEach((entry) => { + process(entry); + }); + + return { + method: "POST", + url: `/api/v1/admin/domain_blocks?import=true`, + asForm: true, + discardEmpty: true, + body: { + domains: new Blob([JSON.stringify(domains)], { type: "application/json" }) + } + }; + }, + transformResponse: domainListToObject, + ...replaceCacheOnMutation("instanceBlocks") + }) +}); + +function entryProcessor(formData) { + let funcs = []; + + ["private_comment", "public_comment"].forEach((type) => { + let text = formData[type].trim(); + + if (text.length > 0) { + let behavior = formData[`${type}_behavior`]; + + if (behavior == "append") { + funcs.push(function appendComment(entry) { + if (entry[type] == undefined) { + entry[type] = text; + } else { + entry[type] = [entry[type], text].join("\n"); + } + }); + } else if (behavior == "replace") { + funcs.push(function replaceComment(entry) { + entry[type] = text; + }); + } + } + }); + + return function process(entry) { + funcs.forEach((func) => { + func(entry); + }); + + entry.obfuscate = formData.obfuscate; + + Object.entries(entry).forEach(([key, val]) => { + if (val == undefined) { + delete entry[key]; + } + }); + }; +}
\ No newline at end of file diff --git a/web/source/settings/lib/query/admin/index.js b/web/source/settings/lib/query/admin/index.js new file mode 100644 index 000000000..33e14521e --- /dev/null +++ b/web/source/settings/lib/query/admin/index.js @@ -0,0 +1,84 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +"use strict"; + +const { + replaceCacheOnMutation, + removeFromCacheOnMutation, + domainListToObject +} = require("../lib"); +const base = require("../base"); + +const endpoints = (build) => ({ + updateInstance: build.mutation({ + query: (formData) => ({ + method: "PATCH", + url: `/api/v1/instance`, + asForm: true, + body: formData, + discardEmpty: true + }), + ...replaceCacheOnMutation("instance") + }), + mediaCleanup: build.mutation({ + query: (days) => ({ + method: "POST", + url: `/api/v1/admin/media_cleanup`, + params: { + remote_cache_days: days + } + }) + }), + instanceBlocks: build.query({ + query: () => ({ + url: `/api/v1/admin/domain_blocks` + }), + transformResponse: domainListToObject + }), + addInstanceBlock: build.mutation({ + query: (formData) => ({ + method: "POST", + url: `/api/v1/admin/domain_blocks`, + asForm: true, + body: formData, + discardEmpty: true + }), + transformResponse: (data) => { + return { + [data.domain]: data + }; + }, + ...replaceCacheOnMutation("instanceBlocks") + }), + removeInstanceBlock: build.mutation({ + query: (id) => ({ + method: "DELETE", + url: `/api/v1/admin/domain_blocks/${id}`, + }), + ...removeFromCacheOnMutation("instanceBlocks", { + findKey: (_draft, newData) => { + return newData.domain; + } + }) + }), + ...require("./import-export")(build), + ...require("./custom-emoji")(build) +}); + +module.exports = base.injectEndpoints({ endpoints });
\ No newline at end of file diff --git a/web/source/settings/lib/query/base.js b/web/source/settings/lib/query/base.js index a782be54a..05d31925f 100644 --- a/web/source/settings/lib/query/base.js +++ b/web/source/settings/lib/query/base.js @@ -1,35 +1,57 @@ /* - GoToSocial - Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org - 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 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. + 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/>. + 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/>. */ "use strict"; const { createApi, fetchBaseQuery } = require("@reduxjs/toolkit/query/react"); +const { isPlainObject } = require("is-plain-object"); -const { convertToForm } = require("../api"); +function convertToForm(obj) { + const formData = new FormData(); + Object.entries(obj).forEach(([key, val]) => { + if (isPlainObject(val)) { + Object.entries(val).forEach(([key2, val2]) => { + if (val2 != undefined) { + formData.set(`${key}[${key2}]`, val2); + } + }); + } else if (val != undefined) { + formData.set(key, val); + } + }); + return formData; +} function instanceBasedQuery(args, api, extraOptions) { const state = api.getState(); - const {instance, token} = state.oauth; + const { instance, token } = state.oauth; if (args.baseUrl == undefined) { args.baseUrl = instance; } + if (args.discardEmpty) { + if (args.body == undefined || Object.keys(args.body).length == 0) { + return { data: null }; + } + delete args.discardEmpty; + } + if (args.asForm) { delete args.asForm; args.body = convertToForm(args.body); @@ -50,6 +72,12 @@ function instanceBasedQuery(args, api, extraOptions) { module.exports = createApi({ reducerPath: "api", baseQuery: instanceBasedQuery, - tagTypes: ["Emojis"], - endpoints: () => ({}) + tagTypes: ["Auth"], + endpoints: (build) => ({ + instance: build.query({ + query: () => ({ + url: `/api/v1/instance` + }) + }) + }) });
\ No newline at end of file diff --git a/web/source/settings/lib/query/custom-emoji.js b/web/source/settings/lib/query/custom-emoji.js deleted file mode 100644 index e62931cb2..000000000 --- a/web/source/settings/lib/query/custom-emoji.js +++ /dev/null @@ -1,180 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org - - 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/>. -*/ - -"use strict"; - -const Promise = require("bluebird"); - -const base = require("./base"); - -function unwrap(res) { - if (res.error != undefined) { - throw res.error; - } else { - return res.data; - } -} - -const endpoints = (build) => ({ - getAllEmoji: build.query({ - query: (params = {}) => ({ - url: "/api/v1/admin/custom_emojis", - params: { - limit: 0, - ...params - } - }), - providesTags: (res) => - res - ? [...res.map((emoji) => ({type: "Emojis", id: emoji.id})), {type: "Emojis", id: "LIST"}] - : [{type: "Emojis", id: "LIST"}] - }), - getEmoji: build.query({ - query: (id) => ({ - url: `/api/v1/admin/custom_emojis/${id}` - }), - providesTags: (res, error, id) => [{type: "Emojis", id}] - }), - addEmoji: build.mutation({ - query: (form) => { - return { - method: "POST", - url: `/api/v1/admin/custom_emojis`, - asForm: true, - body: form - }; - }, - invalidatesTags: (res) => - res - ? [{type: "Emojis", id: "LIST"}, {type: "Emojis", id: res.id}] - : [{type: "Emojis", id: "LIST"}] - }), - editEmoji: build.mutation({ - query: ({id, ...patch}) => { - return { - method: "PATCH", - url: `/api/v1/admin/custom_emojis/${id}`, - asForm: true, - body: { - type: "modify", - ...patch - } - }; - }, - invalidatesTags: (res) => - res - ? [{type: "Emojis", id: "LIST"}, {type: "Emojis", id: res.id}] - : [{type: "Emojis", id: "LIST"}] - }), - deleteEmoji: build.mutation({ - query: (id) => ({ - method: "DELETE", - url: `/api/v1/admin/custom_emojis/${id}` - }), - invalidatesTags: (res, error, id) => [{type: "Emojis", id}] - }), - searchStatusForEmoji: build.mutation({ - query: (url) => ({ - method: "GET", - url: `/api/v2/search?q=${encodeURIComponent(url)}&resolve=true&limit=1` - }), - transformResponse: (res) => { - /* Parses search response, prioritizing a toot result, - and returns referenced custom emoji - */ - let type; - - if (res.statuses.length > 0) { - type = "statuses"; - } else if (res.accounts.length > 0) { - type = "accounts"; - } else { - return { - type: "none" - }; - } - - let data = res[type][0]; - - return { - type, - domain: (new URL(data.url)).host, // to get WEB_DOMAIN, see https://github.com/superseriousbusiness/gotosocial/issues/1225 - list: data.emojis - }; - } - }), - patchRemoteEmojis: build.mutation({ - queryFn: ({action, domain, list, category}, api, _extraOpts, baseQuery) => { - const data = []; - const errors = []; - - return Promise.each(list, (emoji) => { - return Promise.try(() => { - return baseQuery({ - method: "GET", - url: `/api/v1/admin/custom_emojis`, - params: { - filter: `domain:${domain},shortcode:${emoji.shortcode}`, - limit: 1 - } - }).then(unwrap); - }).then(([lookup]) => { - if (lookup == undefined) { throw "not found"; } - - let body = { - type: action - }; - - if (action == "copy") { - body.shortcode = emoji.localShortcode ?? emoji.shortcode; - if (category.trim().length != 0) { - body.category = category; - } - } - - return baseQuery({ - method: "PATCH", - url: `/api/v1/admin/custom_emojis/${lookup.id}`, - asForm: true, - body: body - }).then(unwrap); - }).then((res) => { - data.push([emoji.shortcode, res]); - }).catch((e) => { - console.error("emoji lookup for", emoji.shortcode, "failed:", e); - let msg = e.message ?? e; - if (e.data.error) { - msg = e.data.error; - } - errors.push([emoji.shortcode, msg]); - }); - }).then(() => { - if (errors.length == 0) { - return { data }; - } else { - return { - error: errors - }; - } - }); - }, - invalidatesTags: () => [{type: "Emojis", id: "LIST"}] - }) -}); - -module.exports = base.injectEndpoints({endpoints});
\ No newline at end of file diff --git a/web/source/settings/lib/query/index.js b/web/source/settings/lib/query/index.js index 4bd0ff100..a8f275da7 100644 --- a/web/source/settings/lib/query/index.js +++ b/web/source/settings/lib/query/index.js @@ -1,24 +1,26 @@ /* - GoToSocial - Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org - 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 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. + 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/>. + 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/>. */ "use strict"; module.exports = { ...require("./base"), - ...require("./custom-emoji.js") + ...require("./oauth"), + ...require("./user"), + ...require("./admin") };
\ No newline at end of file diff --git a/web/source/settings/lib/query/lib.js b/web/source/settings/lib/query/lib.js new file mode 100644 index 000000000..dae749198 --- /dev/null +++ b/web/source/settings/lib/query/lib.js @@ -0,0 +1,75 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +"use strict"; + +const syncpipe = require("syncpipe"); +const base = require("./base"); + +module.exports = { + unwrapRes(res) { + if (res.error != undefined) { + throw res.error; + } else { + return res.data; + } + }, + domainListToObject: (data) => { + // Turn flat Array into Object keyed by block's domain + return syncpipe(data, [ + (_) => _.map((entry) => [entry.domain, entry]), + (_) => Object.fromEntries(_) + ]); + }, + replaceCacheOnMutation: makeCacheMutation((draft, newData) => { + Object.assign(draft, newData); + }), + appendCacheOnMutation: makeCacheMutation((draft, newData) => { + draft.push(newData); + }), + spliceCacheOnMutation: makeCacheMutation((draft, newData, { key }) => { + draft.splice(key, 1); + }), + updateCacheOnMutation: makeCacheMutation((draft, newData, { key }) => { + draft[key] = newData; + }), + removeFromCacheOnMutation: makeCacheMutation((draft, newData, { key }) => { + delete draft[key]; + }), + editCacheOnMutation: makeCacheMutation((draft, newData, { update }) => { + update(draft, newData); + }) +}; + +// https://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates#pessimistic-updates +function makeCacheMutation(action) { + return function cacheMutation(queryName, { key, findKey, arg, ...opts } = {}) { + return { + onQueryStarted: (_, { dispatch, queryFulfilled }) => { + queryFulfilled.then(({ data: newData }) => { + dispatch(base.util.updateQueryData(queryName, arg, (draft) => { + if (findKey != undefined) { + key = findKey(draft, newData); + } + action(draft, newData, { key, ...opts }); + })); + }); + } + }; + }; +}
\ No newline at end of file diff --git a/web/source/settings/lib/query/oauth.js b/web/source/settings/lib/query/oauth.js new file mode 100644 index 000000000..4fac50429 --- /dev/null +++ b/web/source/settings/lib/query/oauth.js @@ -0,0 +1,158 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); + +const base = require("./base"); +const { unwrapRes } = require("./lib"); +const oauth = require("../../redux/oauth").actions; + +function getSettingsURL() { + /* needed in case the settings interface isn't hosted at /settings but + some subpath like /gotosocial/settings. Other parts of the code don't + take this into account yet so mostly future-proofing. + + Also drops anything past /settings/, because authorization urls that are too long + get rejected by GTS. + */ + let [pre, _past] = window.location.pathname.split("/settings"); + return `${window.location.origin}${pre}/settings`; +} + +const SETTINGS_URL = getSettingsURL(); + +const endpoints = (build) => ({ + verifyCredentials: build.query({ + providesTags: (_res, error) => + error == undefined + ? ["Auth"] + : [], + queryFn: (_arg, api, _extraOpts, baseQuery) => { + const state = api.getState(); + + return Promise.try(() => { + // Process callback code first, if available + if (state.oauth.loginState == "callback") { + let urlParams = new URLSearchParams(window.location.search); + let code = urlParams.get("code"); + + if (code == undefined) { + throw { + message: "Waiting for callback, but no ?code= provided in url." + }; + } else { + let app = state.oauth.registration; + + if (app == undefined || app.client_id == undefined) { + throw { + message: "No stored registration data, can't finish login flow." + }; + } + + return baseQuery({ + method: "POST", + url: "/oauth/token", + body: { + client_id: app.client_id, + client_secret: app.client_secret, + redirect_uri: SETTINGS_URL, + grant_type: "authorization_code", + code: code + } + }).then(unwrapRes).then((token) => { + // remove ?code= from url + window.history.replaceState({}, document.title, window.location.pathname); + api.dispatch(oauth.setToken(token)); + }); + } + } + }).then(() => { + return baseQuery({ + url: `/api/v1/accounts/verify_credentials` + }); + }).catch((e) => { + return { error: e }; + }); + } + }), + authorizeFlow: build.mutation({ + queryFn: (formData, api, _extraOpts, baseQuery) => { + let instance; + const state = api.getState(); + + return Promise.try(() => { + if (!formData.instance.startsWith("http")) { + formData.instance = `https://${formData.instance}`; + } + instance = new URL(formData.instance).origin; + + const stored = state.oauth.instance; + if (stored?.instance == instance && stored.registration) { + return stored.registration; + } + + return baseQuery({ + method: "POST", + baseUrl: instance, + url: "/api/v1/apps", + body: { + client_name: "GoToSocial Settings", + scopes: formData.scopes, + redirect_uris: SETTINGS_URL, + website: SETTINGS_URL + } + }).then(unwrapRes).then((app) => { + app.scopes = formData.scopes; + + api.dispatch(oauth.setInstance({ + instance: instance, + registration: app, + loginState: "callback" + })); + + return app; + }); + }).then((app) => { + let url = new URL(instance); + url.pathname = "/oauth/authorize"; + url.searchParams.set("client_id", app.client_id); + url.searchParams.set("redirect_uri", SETTINGS_URL); + url.searchParams.set("response_type", "code"); + url.searchParams.set("scope", app.scopes); + + let redirectURL = url.toString(); + window.location.assign(redirectURL); + + return { data: null }; + }).catch((e) => { + return { error: e }; + }); + }, + }), + logout: build.mutation({ + queryFn: (_arg, api) => { + api.dispatch(oauth.remove()); + return { data: null }; + }, + invalidatesTags: ["Auth"] + }) +}); + +module.exports = base.injectEndpoints({ endpoints });
\ No newline at end of file diff --git a/web/source/settings/lib/query/user.js b/web/source/settings/lib/query/user.js new file mode 100644 index 000000000..d2d2830b7 --- /dev/null +++ b/web/source/settings/lib/query/user.js @@ -0,0 +1,44 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +"use strict"; + +const { replaceCacheOnMutation } = require("./lib"); +const base = require("./base"); + +const endpoints = (build) => ({ + updateCredentials: build.mutation({ + query: (formData) => ({ + method: "PATCH", + url: `/api/v1/accounts/update_credentials`, + asForm: true, + body: formData, + discardEmpty: true + }), + ...replaceCacheOnMutation("verifyCredentials") + }), + passwordChange: build.mutation({ + query: (data) => ({ + method: "POST", + url: `/api/v1/user/password_change`, + body: data + }) + }) +}); + +module.exports = base.injectEndpoints({ endpoints });
\ No newline at end of file |