diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/Informations/tests/index.test.js b/packages/core/admin/admin/src/content-manager/pages/EditView/Informations/tests/index.test.js index ee988409cd..12dbc3126e 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/Informations/tests/index.test.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/Informations/tests/index.test.js @@ -15,6 +15,7 @@ import Informations from '../index'; jest.mock('@strapi/helper-plugin', () => ({ useCMEditViewDataManager: jest.fn(), + wrapAxiosInstance: jest.fn(() => {}), })); const makeApp = () => { diff --git a/packages/core/admin/admin/src/core/utils/axiosInstance.js b/packages/core/admin/admin/src/core/utils/axiosInstance.js index 156a5cfa6c..c5a78174d0 100644 --- a/packages/core/admin/admin/src/core/utils/axiosInstance.js +++ b/packages/core/admin/admin/src/core/utils/axiosInstance.js @@ -1,5 +1,5 @@ import axios from 'axios'; -import { auth } from '@strapi/helper-plugin'; +import { auth, wrapAxiosInstance } from '@strapi/helper-plugin'; const instance = axios.create({ baseURL: process.env.STRAPI_ADMIN_BACKEND_URL, @@ -33,4 +33,6 @@ instance.interceptors.response.use( } ); -export default instance; +const wrapper = wrapAxiosInstance(instance); + +export default wrapper; diff --git a/packages/core/admin/admin/src/hooks/index.js b/packages/core/admin/admin/src/hooks/index.js index d602779ed3..c0dd8c6035 100644 --- a/packages/core/admin/admin/src/hooks/index.js +++ b/packages/core/admin/admin/src/hooks/index.js @@ -10,3 +10,4 @@ export { default as usePermissionsDataManager } from './usePermissionsDataManage export { default as useReleaseNotification } from './useReleaseNotification'; export { default as useThemeToggle } from './useThemeToggle'; export { default as useRegenerate } from './useRegenerate'; +export { default as useFetchClient } from './useFetchClient'; diff --git a/packages/core/admin/admin/src/hooks/useFetchClient/index.js b/packages/core/admin/admin/src/hooks/useFetchClient/index.js new file mode 100644 index 0000000000..5ee19bea28 --- /dev/null +++ b/packages/core/admin/admin/src/hooks/useFetchClient/index.js @@ -0,0 +1,23 @@ +import { useEffect, useRef } from 'react'; +import { getFetchClient } from '../../utils/getFetchClient'; + +const useFetchClient = () => { + const controller = useRef(null); + + if (controller.current === null) { + controller.current = new AbortController(); + } + useEffect(() => { + return () => { + controller.current.abort(); + }; + }, []); + + const defaultOptions = { + signal: controller.current.signal, + }; + + return getFetchClient(defaultOptions); +}; + +export default useFetchClient; diff --git a/packages/core/admin/admin/src/pages/Admin/tests/useTrackUsage.test.js b/packages/core/admin/admin/src/pages/Admin/tests/useTrackUsage.test.js index 52bbda94f6..0b64a2ac65 100644 --- a/packages/core/admin/admin/src/pages/Admin/tests/useTrackUsage.test.js +++ b/packages/core/admin/admin/src/pages/Admin/tests/useTrackUsage.test.js @@ -11,6 +11,7 @@ jest.mock('react-redux', () => ({ jest.mock('@strapi/helper-plugin', () => ({ useTracking: jest.fn(() => ({ trackUsage: trackUsageMock })), + wrapAxiosInstance: jest.fn(() => {}), })); describe('Admin | pages | AdminĀ | useTrackUsage', () => { diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/index.js index 46b5db5104..8b33a5e0ea 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/index.js @@ -34,27 +34,32 @@ const ModalForm = ({ queryName, onToggle }) => { const { formatMessage } = useIntl(); const toggleNotification = useNotification(); const { lockApp, unlockApp } = useOverlayBlocker(); - const postMutation = useMutation((body) => axiosInstance.post('/admin/users', body), { - async onSuccess({ data }) { - setRegistrationToken(data.data.registrationToken); - await queryClient.invalidateQueries(queryName); - goNext(); - setIsSubmitting(false); + const postMutation = useMutation( + (body) => { + return axiosInstance.post('/admin/users', body); }, - onError(err) { - setIsSubmitting(false); + { + async onSuccess({ data }) { + setRegistrationToken(data.data.registrationToken); + await queryClient.invalidateQueries(queryName); + goNext(); + setIsSubmitting(false); + }, + onError(err) { + setIsSubmitting(false); - toggleNotification({ - type: 'warning', - message: { id: 'notification.error', defaultMessage: 'An error occured' }, - }); + toggleNotification({ + type: 'warning', + message: { id: 'notification.error', defaultMessage: 'An error occured' }, + }); - throw err; - }, - onSettled() { - unlockApp(); - }, - }); + throw err; + }, + onSettled() { + unlockApp(); + }, + } + ); const headerTitle = formatMessage({ id: 'Settings.permissions.users.create', diff --git a/packages/core/admin/admin/src/utils/fetchClient.js b/packages/core/admin/admin/src/utils/fetchClient.js new file mode 100644 index 0000000000..51568ef2c1 --- /dev/null +++ b/packages/core/admin/admin/src/utils/fetchClient.js @@ -0,0 +1,45 @@ +import axios from 'axios'; +import { auth } from '@strapi/helper-plugin'; + +export const reqInterceptor = async (config) => { + config.headers = { + Authorization: `Bearer ${auth.getToken()}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + + return config; +}; + +export const reqErrorInterceptor = (error) => { + return Promise.reject(error); +}; + +export const resInterceptor = (response) => response; + +export const resErrorInterceptor = (error) => { + // whatever you want to do with the error + if (error?.response?.status === 401) { + auth.clearAppStorage(); + window.location.reload(); + } + + throw error; +}; + +export const addInterceptors = (instance) => { + instance.interceptors.request.use(reqInterceptor, reqErrorInterceptor); + + instance.interceptors.response.use(resInterceptor, resErrorInterceptor); +}; + +export const fetchClient = ({ baseURL }) => { + const instance = axios.create({ + baseURL, + }); + addInterceptors(instance); + + return instance; +}; + +export default fetchClient({ baseURL: process.env.STRAPI_ADMIN_BACKEND_URL }); diff --git a/packages/core/admin/admin/src/utils/getFetchClient.js b/packages/core/admin/admin/src/utils/getFetchClient.js new file mode 100644 index 0000000000..c0924e9956 --- /dev/null +++ b/packages/core/admin/admin/src/utils/getFetchClient.js @@ -0,0 +1,10 @@ +import instance from './fetchClient'; + +export const getFetchClient = (defaultOptions = {}) => { + return { + get: (url, config) => instance.get(url, { ...defaultOptions, ...config }), + put: (url, data, config) => instance.put(url, data, { ...defaultOptions, ...config }), + post: (url, data, config) => instance.post(url, data, { ...defaultOptions, ...config }), + del: (url, config) => instance.delete(url, { ...defaultOptions, ...config }), + }; +}; diff --git a/packages/core/admin/admin/src/utils/tests/fetchClient.test.js b/packages/core/admin/admin/src/utils/tests/fetchClient.test.js new file mode 100644 index 0000000000..6b731b45dd --- /dev/null +++ b/packages/core/admin/admin/src/utils/tests/fetchClient.test.js @@ -0,0 +1,100 @@ +import { AxiosError } from 'axios'; +import { auth } from '@strapi/helper-plugin'; +import { + reqInterceptor, + reqErrorInterceptor, + resInterceptor, + resErrorInterceptor, + fetchClient, + addInterceptors, +} from '../fetchClient'; + +const token = 'coolToken'; +auth.getToken = jest.fn().mockReturnValue(token); +auth.clearAppStorage = jest.fn().mockReturnValue(token); + +describe('ADMIN | utils | fetchClient', () => { + describe('Test the interceptors', () => { + it('API request should add authorization token to header', async () => { + const apiInstance = fetchClient({ + baseUrl: 'http://strapi', + }); + const result = await apiInstance.interceptors.request.handlers[0].fulfilled({ headers: {} }); + expect(result.headers.Authorization).toContain(`Bearer ${token}`); + expect(result.headers.Accept).toBe('application/json'); + expect(apiInstance.interceptors.response.handlers[0].fulfilled('foo')).toBe('foo'); + }); + describe('Test the addInterceptor function', () => { + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); + it('should add a response interceptor to the fetchClient instance', () => { + const apiInstance = fetchClient({ + baseUrl: 'http://strapi-test', + }); + const spyReq = jest.spyOn(apiInstance.interceptors.request, 'use'); + const spyRes = jest.spyOn(apiInstance.interceptors.response, 'use'); + addInterceptors(apiInstance); + expect(spyReq).toHaveBeenCalled(); + expect(spyRes).toHaveBeenCalled(); + }); + }); + }); + describe('Test the interceptors callbacks', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + configurable: true, + value: { reload: jest.fn() }, + }); + }); + afterAll(() => { + Object.defineProperty(window, 'location', { configurable: true, value: window.location }); + }); + it('should return the config object passed with the correct headers the request interceptor callback on success', async () => { + const configMock = { + headers: { + common: { Accept: 'application/json, text/plain, */*' }, + delete: {}, + get: {}, + head: {}, + post: { 'Content-Type': 'application/x-www-form-urlencoded' }, + put: { 'Content-Type': 'application/x-www-form-urlencoded' }, + patch: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }, + method: 'get', + url: '/test', + }; + const configResponse = await reqInterceptor(configMock); + expect(configResponse.headers.Authorization).toBe(`Bearer ${token}`); + }); + }); + it('should throw an error when the request interceptor error callback is called', () => { + expect(reqErrorInterceptor('test')).rejects.toBe('test'); + expect(reqErrorInterceptor(new Error('test error'))).rejects.toThrow(new Error('test error')); + }); + it('should return the response when the result interceptor callback is called', () => { + const response = { + msg: 'I am a response', + }; + expect(resInterceptor(response).msg).toBe('I am a response'); + }); + it('should trigger the auth clearAppStorage and the window.location.reload when the result interceptor error callback is called', () => { + const error = new AxiosError('Unauthorized'); + error.config = {}; + error.request = {}; + error.response = { + data: { data: null, error: [Object] }, + status: 401, + statusText: 'Unauthorized', + }; + jest.spyOn(window.location, 'reload'); + try { + resErrorInterceptor(error); + } catch (error) { + expect(error.response.status).toBe(401); + expect(auth.clearAppStorage).toHaveBeenCalledTimes(1); + expect(window.location.reload).toHaveBeenCalledTimes(1); + } + }); +}); diff --git a/packages/core/admin/admin/src/utils/tests/getFetchClient.test.js b/packages/core/admin/admin/src/utils/tests/getFetchClient.test.js new file mode 100644 index 0000000000..92635db186 --- /dev/null +++ b/packages/core/admin/admin/src/utils/tests/getFetchClient.test.js @@ -0,0 +1,25 @@ +import { auth } from '@strapi/helper-plugin'; +import { getFetchClient } from '../getFetchClient'; + +const token = 'coolToken'; +auth.getToken = jest.fn().mockReturnValue(token); + +describe('ADMIN | utils | getFetchClient', () => { + it('should return the 4 HTTP methods to call GET, POST, PUT and DELETE apis', () => { + const response = getFetchClient(); + expect(response).toHaveProperty('get'); + expect(response).toHaveProperty('post'); + expect(response).toHaveProperty('put'); + expect(response).toHaveProperty('del'); + }); + it('should contain the headers config values and the data when we try to reach an unknown API', async () => { + const response = getFetchClient(); + try { + await response.get('/test'); + } catch (err) { + const { headers } = err.config; + expect(headers.Authorization).toContain(`Bearer ${token}`); + expect(headers.Accept).toBe('application/json'); + } + }); +}); diff --git a/packages/core/content-type-builder/admin/src/components/DataManagerProvider/index.js b/packages/core/content-type-builder/admin/src/components/DataManagerProvider/index.js index f92b694e32..bd030e2dc9 100644 --- a/packages/core/content-type-builder/admin/src/components/DataManagerProvider/index.js +++ b/packages/core/content-type-builder/admin/src/components/DataManagerProvider/index.js @@ -108,6 +108,7 @@ const DataManagerProvider = ({ { data: reservedNames }, ] = await Promise.all( ['components', 'content-types', 'reserved-names'].map((endPoint) => { + // TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient return axiosInstance.get(endPoint); }) ); @@ -265,7 +266,7 @@ const DataManagerProvider = ({ if (userConfirm) { lockAppWithAutoreload(); - + // TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient await axiosInstance.delete(requestURL); // Make sure the server has restarted @@ -315,7 +316,7 @@ const DataManagerProvider = ({ } lockAppWithAutoreload(); - + // TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient await axiosInstance.delete(requestURL); // Make sure the server has restarted @@ -349,6 +350,7 @@ const DataManagerProvider = ({ lockAppWithAutoreload(); // Update the category + // TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient await axiosInstance({ url: requestURL, method: 'PUT', data: body }); // Make sure the server has restarted @@ -506,7 +508,7 @@ const DataManagerProvider = ({ // Lock the app lockAppWithAutoreload(); - + // TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient await axiosInstance({ url: requestURL, method, diff --git a/packages/core/content-type-builder/admin/src/utils/axiosInstance.js b/packages/core/content-type-builder/admin/src/utils/axiosInstance.js index 03f0cf333a..6de08f4c44 100644 --- a/packages/core/content-type-builder/admin/src/utils/axiosInstance.js +++ b/packages/core/content-type-builder/admin/src/utils/axiosInstance.js @@ -1,5 +1,6 @@ import axios from 'axios'; -import { auth } from '@strapi/helper-plugin'; +import { auth, wrapAxiosInstance } from '@strapi/helper-plugin'; +// TODO: remember to pass also the pluginId when you use the new get, post, put, delete methods from getFetchClient import pluginId from '../pluginId'; const instance = axios.create({ @@ -34,4 +35,6 @@ instance.interceptors.response.use( } ); -export default instance; +const wrapper = wrapAxiosInstance(instance); + +export default wrapper; diff --git a/packages/core/email/admin/src/utils/axiosInstance.js b/packages/core/email/admin/src/utils/axiosInstance.js index 8abf430c89..3210d5ebe9 100644 --- a/packages/core/email/admin/src/utils/axiosInstance.js +++ b/packages/core/email/admin/src/utils/axiosInstance.js @@ -1,5 +1,5 @@ import axios from 'axios'; -import { auth } from '@strapi/helper-plugin'; +import { auth, wrapAxiosInstance } from '@strapi/helper-plugin'; const instance = axios.create({ baseURL: process.env.STRAPI_ADMIN_BACKEND_URL, @@ -33,4 +33,6 @@ instance.interceptors.response.use( } ); -export default instance; +const wrapper = wrapAxiosInstance(instance); + +export default wrapper; diff --git a/packages/core/helper-plugin/lib/src/index.js b/packages/core/helper-plugin/lib/src/index.js index 1bd0e4f6ab..2c604b2508 100644 --- a/packages/core/helper-plugin/lib/src/index.js +++ b/packages/core/helper-plugin/lib/src/index.js @@ -95,6 +95,7 @@ export { default as contentManagementUtilRemoveFieldsFromData } from './content- export { default as getFileExtension } from './utils/getFileExtension/getFileExtension'; export * from './utils/stopPropagation'; export { default as difference } from './utils/difference'; +export { default as wrapAxiosInstance } from './utils/wrapAxiosInstance'; export { default as request } from './utils/request'; export { default as getAPIInnerErrors } from './utils/getAPIInnerErrors'; diff --git a/packages/core/helper-plugin/lib/src/utils/wrapAxiosInstance/index.js b/packages/core/helper-plugin/lib/src/utils/wrapAxiosInstance/index.js new file mode 100644 index 0000000000..3770d8486e --- /dev/null +++ b/packages/core/helper-plugin/lib/src/utils/wrapAxiosInstance/index.js @@ -0,0 +1,19 @@ +function wrapAxiosInstance(instance) { + if (process.env.NODE_ENV !== 'development') return instance; + const wrapper = {}; + ['request', 'get', 'head', 'delete', 'options', 'post', 'put', 'patch', 'getUri'].forEach( + (methodName) => { + wrapper[methodName] = (...args) => { + console.log( + 'Deprecation warning: Usage of "axiosInstance" utility is deprecated and will be removed in the next major release. Instead, use the useFetchClient() hook, which is exported from the admin: { useFetchClient } from "@strapi/helper-plugin"' + ); + + return instance[methodName](...args); + }; + } + ); + + return wrapper; +} + +export default wrapAxiosInstance; diff --git a/packages/generators/generators/lib/files/js/plugin/admin/src/utils/axiosInstance.js b/packages/generators/generators/lib/files/js/plugin/admin/src/utils/axiosInstance.js index 3d03d75c18..e487f8c4ce 100644 --- a/packages/generators/generators/lib/files/js/plugin/admin/src/utils/axiosInstance.js +++ b/packages/generators/generators/lib/files/js/plugin/admin/src/utils/axiosInstance.js @@ -3,7 +3,7 @@ */ import axios from 'axios'; -import { auth } from '@strapi/helper-plugin'; +import { auth, wrapAxiosInstance } from '@strapi/helper-plugin'; const instance = axios.create({ baseURL: process.env.STRAPI_ADMIN_BACKEND_URL, @@ -37,4 +37,6 @@ instance.interceptors.response.use( } ); -export default instance; +const wrapper = wrapAxiosInstance(instance); + +export default wrapper; diff --git a/packages/generators/generators/lib/files/ts/plugin/admin/src/utils/axiosInstance.ts b/packages/generators/generators/lib/files/ts/plugin/admin/src/utils/axiosInstance.ts index 3d03d75c18..e487f8c4ce 100644 --- a/packages/generators/generators/lib/files/ts/plugin/admin/src/utils/axiosInstance.ts +++ b/packages/generators/generators/lib/files/ts/plugin/admin/src/utils/axiosInstance.ts @@ -3,7 +3,7 @@ */ import axios from 'axios'; -import { auth } from '@strapi/helper-plugin'; +import { auth, wrapAxiosInstance } from '@strapi/helper-plugin'; const instance = axios.create({ baseURL: process.env.STRAPI_ADMIN_BACKEND_URL, @@ -37,4 +37,6 @@ instance.interceptors.response.use( } ); -export default instance; +const wrapper = wrapAxiosInstance(instance); + +export default wrapper; diff --git a/packages/plugins/i18n/admin/src/utils/axiosInstance.js b/packages/plugins/i18n/admin/src/utils/axiosInstance.js index 8abf430c89..3210d5ebe9 100644 --- a/packages/plugins/i18n/admin/src/utils/axiosInstance.js +++ b/packages/plugins/i18n/admin/src/utils/axiosInstance.js @@ -1,5 +1,5 @@ import axios from 'axios'; -import { auth } from '@strapi/helper-plugin'; +import { auth, wrapAxiosInstance } from '@strapi/helper-plugin'; const instance = axios.create({ baseURL: process.env.STRAPI_ADMIN_BACKEND_URL, @@ -33,4 +33,6 @@ instance.interceptors.response.use( } ); -export default instance; +const wrapper = wrapAxiosInstance(instance); + +export default wrapper; diff --git a/packages/plugins/users-permissions/admin/src/hooks/useFetchRole/index.js b/packages/plugins/users-permissions/admin/src/hooks/useFetchRole/index.js index febfed35b7..578bd1e0c7 100644 --- a/packages/plugins/users-permissions/admin/src/hooks/useFetchRole/index.js +++ b/packages/plugins/users-permissions/admin/src/hooks/useFetchRole/index.js @@ -1,7 +1,7 @@ import { useCallback, useReducer, useEffect, useRef } from 'react'; import { useNotification } from '@strapi/helper-plugin'; import reducer, { initialState } from './reducer'; -import axiosIntance from '../../utils/axiosInstance'; +import axiosInstance from '../../utils/axiosInstance'; import pluginId from '../../pluginId'; const useFetchRole = (id) => { @@ -29,7 +29,7 @@ const useFetchRole = (id) => { try { const { data: { role }, - } = await axiosIntance.get(`/${pluginId}/roles/${roleId}`); + } = await axiosInstance.get(`/${pluginId}/roles/${roleId}`); // Prevent updating state on an unmounted component if (isMounted.current) { diff --git a/packages/plugins/users-permissions/admin/src/utils/axiosInstance.js b/packages/plugins/users-permissions/admin/src/utils/axiosInstance.js index 8abf430c89..3210d5ebe9 100644 --- a/packages/plugins/users-permissions/admin/src/utils/axiosInstance.js +++ b/packages/plugins/users-permissions/admin/src/utils/axiosInstance.js @@ -1,5 +1,5 @@ import axios from 'axios'; -import { auth } from '@strapi/helper-plugin'; +import { auth, wrapAxiosInstance } from '@strapi/helper-plugin'; const instance = axios.create({ baseURL: process.env.STRAPI_ADMIN_BACKEND_URL, @@ -33,4 +33,6 @@ instance.interceptors.response.use( } ); -export default instance; +const wrapper = wrapAxiosInstance(instance); + +export default wrapper;