diff options
Diffstat (limited to 'web/source/settings/lib/query')
-rw-r--r-- | web/source/settings/lib/query/admin/custom-emoji.js | 2 | ||||
-rw-r--r-- | web/source/settings/lib/query/admin/import-export.js | 2 | ||||
-rw-r--r-- | web/source/settings/lib/query/admin/index.js | 6 | ||||
-rw-r--r-- | web/source/settings/lib/query/admin/reports.js | 2 | ||||
-rw-r--r-- | web/source/settings/lib/query/base.js | 70 | ||||
-rw-r--r-- | web/source/settings/lib/query/gts-api.ts | 149 | ||||
-rw-r--r-- | web/source/settings/lib/query/index.js | 6 | ||||
-rw-r--r-- | web/source/settings/lib/query/lib.js | 6 | ||||
-rw-r--r-- | web/source/settings/lib/query/oauth.js | 160 | ||||
-rw-r--r-- | web/source/settings/lib/query/oauth/index.ts | 204 | ||||
-rw-r--r-- | web/source/settings/lib/query/user/index.ts (renamed from web/source/settings/lib/query/user.js) | 43 |
11 files changed, 382 insertions, 268 deletions
diff --git a/web/source/settings/lib/query/admin/custom-emoji.js b/web/source/settings/lib/query/admin/custom-emoji.js index c84155efd..6e7c772a2 100644 --- a/web/source/settings/lib/query/admin/custom-emoji.js +++ b/web/source/settings/lib/query/admin/custom-emoji.js @@ -17,8 +17,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -"use strict"; - const Promise = require("bluebird"); const { unwrapRes } = require("../lib"); diff --git a/web/source/settings/lib/query/admin/import-export.js b/web/source/settings/lib/query/admin/import-export.js index 7c44d8280..9a04438c2 100644 --- a/web/source/settings/lib/query/admin/import-export.js +++ b/web/source/settings/lib/query/admin/import-export.js @@ -17,8 +17,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -"use strict"; - const Promise = require("bluebird"); const fileDownload = require("js-file-download"); const csv = require("papaparse"); diff --git a/web/source/settings/lib/query/admin/index.js b/web/source/settings/lib/query/admin/index.js index 7b46e6ba4..7a55389d3 100644 --- a/web/source/settings/lib/query/admin/index.js +++ b/web/source/settings/lib/query/admin/index.js @@ -17,15 +17,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -"use strict"; - const { replaceCacheOnMutation, removeFromCacheOnMutation, domainListToObject, idListToObject } = require("../lib"); -const base = require("../base"); +const { gtsApi } = require("../gts-api"); const endpoints = (build) => ({ updateInstance: build.mutation({ @@ -164,4 +162,4 @@ const endpoints = (build) => ({ ...require("./reports")(build) }); -module.exports = base.injectEndpoints({ endpoints });
\ No newline at end of file +module.exports = gtsApi.injectEndpoints({ endpoints });
\ No newline at end of file diff --git a/web/source/settings/lib/query/admin/reports.js b/web/source/settings/lib/query/admin/reports.js index 28940872c..1c45bb7bc 100644 --- a/web/source/settings/lib/query/admin/reports.js +++ b/web/source/settings/lib/query/admin/reports.js @@ -17,8 +17,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -"use strict"; - module.exports = (build) => ({ listReports: build.query({ query: (params = {}) => ({ diff --git a/web/source/settings/lib/query/base.js b/web/source/settings/lib/query/base.js deleted file mode 100644 index ba02d4e07..000000000 --- a/web/source/settings/lib/query/base.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - 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/>. -*/ - -"use strict"; - -const { createApi, fetchBaseQuery } = require("@reduxjs/toolkit/query/react"); -const { serialize: serializeForm } = require("object-to-formdata"); - -function instanceBasedQuery(args, api, extraOptions) { - const state = api.getState(); - 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 = serializeForm(args.body, { - indices: true, // Array indices, for profile fields - }); - } - - return fetchBaseQuery({ - baseUrl: args.baseUrl, - prepareHeaders: (headers) => { - if (token != undefined) { - headers.set('Authorization', token); - } - headers.set("Accept", "application/json"); - return headers; - }, - })(args, api, extraOptions); -} - -module.exports = createApi({ - reducerPath: "api", - baseQuery: instanceBasedQuery, - tagTypes: ["Auth", "Emoji", "Reports", "Account", "InstanceRules"], - endpoints: (build) => ({ - instance: build.query({ - query: () => ({ - url: `/api/v1/instance` - }) - }) - }) -});
\ No newline at end of file diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts new file mode 100644 index 000000000..9e043137c --- /dev/null +++ b/web/source/settings/lib/query/gts-api.ts @@ -0,0 +1,149 @@ +/* + 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 { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import type { + BaseQueryFn, + FetchArgs, + FetchBaseQueryError, +} from '@reduxjs/toolkit/query/react'; +import { serialize as serializeForm } from "object-to-formdata"; + +import type { RootState } from '../../redux/store'; + +/** + * GTSFetchArgs extends standard FetchArgs used by + * RTK Query with a couple helpers of our own. + */ +export interface GTSFetchArgs extends FetchArgs { + /** + * If provided, will be used as base URL. Else, + * will fall back to authorized instance as baseUrl. + */ + baseUrl?: string; + /** + * If true, and no args.body is set, or args.body is empty, + * then a null response will be returned from the API call. + */ + discardEmpty?: boolean; + /** + * If true, then args.body will be serialized + * as FormData before submission. + */ + asForm?: boolean; +} + +/** + * gtsBaseQuery wraps the redux toolkit fetchBaseQuery with some helper functionality. + * + * For an explainer of what's happening in this function, see: + * - https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#customizing-queries-with-basequery + * - https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#constructing-a-dynamic-base-url-using-redux-state + * + * @param args + * @param api + * @param extraOptions + * @returns + */ +const gtsBaseQuery: BaseQueryFn< + string | GTSFetchArgs, + any, + FetchBaseQueryError +> = async (args, api, extraOptions) => { + // Retrieve state at the moment + // this function was called. + const state = api.getState() as RootState; + const { instanceUrl, token } = state.oauth; + + // Derive baseUrl dynamically. + let baseUrl: string; + + // Check if simple string baseUrl provided + // as args, or if more complex args provided. + if (typeof args === "string") { + baseUrl = args; + } else { + if (args.baseUrl != undefined) { + baseUrl = args.baseUrl; + } else { + baseUrl = instanceUrl; + } + + if (args.discardEmpty) { + if (args.body == undefined || Object.keys(args.body).length == 0) { + return { data: null }; + } + } + + if (args.asForm) { + args.body = serializeForm(args.body, { + // Array indices, for profile fields. + indices: true, + }); + } + + // Delete any of our extended arguments + // to avoid confusing fetchBaseQuery. + delete args.baseUrl; + delete args.discardEmpty; + delete args.asForm; + } + + if (!baseUrl) { + return { + error: { + status: 400, + statusText: 'Bad Request', + data: {"error":"No baseUrl set for request"}, + }, + }; + } + + return fetchBaseQuery({ + baseUrl: baseUrl, + prepareHeaders: (headers) => { + if (token != undefined) { + headers.set('Authorization', token); + } + headers.set("Accept", "application/json"); + return headers; + }, + })(args, api, extraOptions); +}; + +export const gtsApi = createApi({ + reducerPath: "api", + baseQuery: gtsBaseQuery, + tagTypes: [ + "Auth", + "Emoji", + "Reports", + "Account", + "InstanceRules", + ], + endpoints: (builder) => ({ + instance: builder.query<any, void>({ + query: () => ({ + url: `/api/v1/instance` + }) + }) + }) +}); + +export const { useInstanceQuery } = gtsApi; diff --git a/web/source/settings/lib/query/index.js b/web/source/settings/lib/query/index.js index dab896f80..aeaa4a1d7 100644 --- a/web/source/settings/lib/query/index.js +++ b/web/source/settings/lib/query/index.js @@ -17,11 +17,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -"use strict"; - module.exports = { - ...require("./base"), + ...require("./gts-api"), ...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 index 56ce05478..1025ca3a7 100644 --- a/web/source/settings/lib/query/lib.js +++ b/web/source/settings/lib/query/lib.js @@ -17,10 +17,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -"use strict"; - const syncpipe = require("syncpipe"); -const base = require("./base"); +const { gtsApi } = require("./gts-api"); module.exports = { unwrapRes(res) { @@ -70,7 +68,7 @@ function makeCacheMutation(action) { return { onQueryStarted: (_, { dispatch, queryFulfilled }) => { queryFulfilled.then(({ data: newData }) => { - dispatch(base.util.updateQueryData(queryName, arg, (draft) => { + dispatch(gtsApi.util.updateQueryData(queryName, arg, (draft) => { if (findKey != undefined) { key = findKey(draft, newData); } diff --git a/web/source/settings/lib/query/oauth.js b/web/source/settings/lib/query/oauth.js deleted file mode 100644 index 7284ee856..000000000 --- a/web/source/settings/lib/query/oauth.js +++ /dev/null @@ -1,160 +0,0 @@ -/* - 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/>. -*/ - -"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.authorize({ - instance: instance, - registration: app, - loginState: "callback", - expectingRedirect: true - })); - - 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/oauth/index.ts b/web/source/settings/lib/query/oauth/index.ts new file mode 100644 index 000000000..9af2dd5fb --- /dev/null +++ b/web/source/settings/lib/query/oauth/index.ts @@ -0,0 +1,204 @@ +/* + 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 type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; + +import { gtsApi } from "../gts-api"; +import { + setToken as oauthSetToken, + remove as oauthRemove, + authorize as oauthAuthorize, +} from "../../../redux/oauth"; +import { RootState } from '../../../redux/store'; + +export interface OauthTokenRequestBody { + client_id: string; + client_secret: string; + redirect_uri: string; + grant_type: string; + code: string; +} + +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()); + +// Couple auth functions here require multiple requests as +// part of an OAuth token 'flow'. To keep things simple for +// callers of these query functions, the multiple requests +// are chained within one query. +// +// https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#performing-multiple-requests-with-a-single-query +const extended = gtsApi.injectEndpoints({ + endpoints: (builder) => ({ + verifyCredentials: builder.query<any, void>({ + providesTags: (_res, error) => + error == undefined ? ["Auth"] : [], + async queryFn(_arg, api, _extraOpts, fetchWithBQ) { + const state = api.getState() as RootState; + const oauthState = state.oauth; + + // If we're not in the middle of an auth/callback, + // we may already have an auth token, so just + // return a standard verify_credentials query. + if (oauthState.loginState != 'callback') { + return fetchWithBQ({ + url: `/api/v1/accounts/verify_credentials` + }); + } + + // We're in the middle of an auth/callback flow. + // Try to retrieve callback code from URL query. + let urlParams = new URLSearchParams(window.location.search); + let code = urlParams.get("code"); + if (code == undefined) { + return { + error: { + status: 400, + statusText: 'Bad Request', + data: {"error":"Waiting for callback, but no ?code= provided in url."}, + }, + }; + } + + // Retrieve app with which the + // callback code was generated. + let app = oauthState.app; + if (app == undefined || app.client_id == undefined) { + return { + error: { + status: 400, + statusText: 'Bad Request', + data: {"error":"No stored app registration data, can't finish login flow."}, + }, + }; + } + + // Use the provided code and app + // secret to request an auth token. + const tokenReqBody: OauthTokenRequestBody = { + client_id: app.client_id, + client_secret: app.client_secret, + redirect_uri: SETTINGS_URL, + grant_type: "authorization_code", + code: code + }; + + const tokenResult = await fetchWithBQ({ + method: "POST", + url: "/oauth/token", + body: tokenReqBody, + }); + if (tokenResult.error) { + return { error: tokenResult.error as FetchBaseQueryError }; + } + + // Remove ?code= query param from + // url, we don't want it anymore. + window.history.replaceState({}, document.title, window.location.pathname); + + // Store returned token in redux. + api.dispatch(oauthSetToken(tokenResult.data)); + + // We're now authed! So return + // standard verify_credentials query. + return fetchWithBQ({ + url: `/api/v1/accounts/verify_credentials` + }); + } + }), + + authorizeFlow: builder.mutation({ + async queryFn(formData, api, _extraOpts, fetchWithBQ) { + const state = api.getState() as RootState; + const oauthState = state.oauth; + + let instanceUrl: string; + if (!formData.instance.startsWith("http")) { + formData.instance = `https://${formData.instance}`; + } + + instanceUrl = new URL(formData.instance).origin; + if (oauthState?.instanceUrl == instanceUrl && oauthState.app) { + return { data: oauthState.app }; + } + + const appResult = await fetchWithBQ({ + method: "POST", + baseUrl: instanceUrl, + url: "/api/v1/apps", + body: { + client_name: "GoToSocial Settings", + scopes: formData.scopes, + redirect_uris: SETTINGS_URL, + website: SETTINGS_URL + } + }); + if (appResult.error) { + return { error: appResult.error as FetchBaseQueryError }; + } + + let app = appResult.data as any; + + app.scopes = formData.scopes; + api.dispatch(oauthAuthorize({ + instanceUrl: instanceUrl, + app: app, + loginState: "callback", + expectingRedirect: true + })); + + let url = new URL(instanceUrl); + 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 }; + }, + }), + logout: builder.mutation({ + queryFn: (_arg, api) => { + api.dispatch(oauthRemove()); + return { data: null }; + }, + invalidatesTags: ["Auth"] + }) + }) +}); + +export const { + useVerifyCredentialsQuery, + useAuthorizeFlowMutation, + useLogoutMutation, +} = extended;
\ No newline at end of file diff --git a/web/source/settings/lib/query/user.js b/web/source/settings/lib/query/user/index.ts index a88e16035..751e38e5b 100644 --- a/web/source/settings/lib/query/user.js +++ b/web/source/settings/lib/query/user/index.ts @@ -17,29 +17,32 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -"use strict"; +import { replaceCacheOnMutation } from "../lib"; +import { gtsApi } from "../gts-api"; -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 +const extended = gtsApi.injectEndpoints({ + endpoints: (builder) => ({ + updateCredentials: builder.mutation({ + query: (formData) => ({ + method: "PATCH", + url: `/api/v1/accounts/update_credentials`, + asForm: true, + body: formData, + discardEmpty: true + }), + ...replaceCacheOnMutation("verifyCredentials") }), - ...replaceCacheOnMutation("verifyCredentials") - }), - passwordChange: build.mutation({ - query: (data) => ({ - method: "POST", - url: `/api/v1/user/password_change`, - body: data + passwordChange: builder.mutation({ + query: (data) => ({ + method: "POST", + url: `/api/v1/user/password_change`, + body: data + }) }) }) }); -module.exports = base.injectEndpoints({ endpoints });
\ No newline at end of file +export const { + useUpdateCredentialsMutation, + usePasswordChangeMutation, +} = extended; |