From 66f131ab146b62357ab3fb7075ae3a11d77a72d1 Mon Sep 17 00:00:00 2001 From: Bartosz Herba Date: Fri, 28 Jan 2022 13:48:03 +0100 Subject: [PATCH] fix(theme, composable): expired user token issue - add proper handling of unathorized requests --- .../src/composables/useAddresses/index.ts | 6 +-- .../src/composables/useBilling/index.ts | 6 +-- .../useGetShippingMethods/index.ts | 2 +- .../src/composables/useReview/index.ts | 8 ++-- .../src/composables/useUser/index.ts | 12 +++-- .../src/composables/useUserBilling/index.ts | 9 ++-- .../src/composables/useUserOrder/index.ts | 2 +- .../src/composables/useUserShipping/index.ts | 8 ++-- .../src/composables/useWishlist/index.ts | 10 ++--- .../composables/useUiNotification/index.ts | 3 +- packages/theme/pages/MyAccount/MyProfile.vue | 6 ++- .../plugins/__tests__/token-expired.spec.js | 40 ++++++++++++----- packages/theme/plugins/token-expired.js | 27 ------------ packages/theme/plugins/token-expired.ts | 44 +++++++++++++++++++ 14 files changed, 111 insertions(+), 72 deletions(-) delete mode 100644 packages/theme/plugins/token-expired.js create mode 100644 packages/theme/plugins/token-expired.ts diff --git a/packages/composables/src/composables/useAddresses/index.ts b/packages/composables/src/composables/useAddresses/index.ts index 48979e7df..f060077a9 100644 --- a/packages/composables/src/composables/useAddresses/index.ts +++ b/packages/composables/src/composables/useAddresses/index.ts @@ -35,7 +35,7 @@ RemoveAddressInput> = { const { data } = await context.$magento.api.getCustomerAddresses(); - return data.customer.addresses; + return data?.customer?.addresses ?? []; }, save: async (context: Context, saveParams) => { Logger.debug('[Magento] save user address:', saveParams.address); @@ -45,7 +45,7 @@ RemoveAddressInput> = { Logger.debug('[Result]:', { data }); - return data.createCustomerAddress; + return data?.createCustomerAddress ?? {}; }, remove: async (context: Context, params) => { Logger.debug('[Magento] remove user addresses'); @@ -67,7 +67,7 @@ RemoveAddressInput> = { Logger.debug('[Result]:', { data }); - return data.updateCustomerAddress; + return data?.updateCustomerAddress ?? {}; }, }; diff --git a/packages/composables/src/composables/useBilling/index.ts b/packages/composables/src/composables/useBilling/index.ts index c1824a117..bffdbc76e 100644 --- a/packages/composables/src/composables/useBilling/index.ts +++ b/packages/composables/src/composables/useBilling/index.ts @@ -25,7 +25,7 @@ const factoryParams: UseBillingParams = { await context.cart.load({ customQuery }); } - return context.cart.cart.value.billing_address; + return context?.cart?.cart?.value?.billing_address ?? {}; }, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -79,11 +79,11 @@ const factoryParams: UseBillingParams = { method_code: shippingMethod.method_code, }, }); + /** * End of GraphQL Workaround */ - - return data.setBillingAddressOnCart.cart.billing_address; + return data?.setBillingAddressOnCart?.cart?.billing_address ?? {}; }, }; diff --git a/packages/composables/src/composables/useGetShippingMethods/index.ts b/packages/composables/src/composables/useGetShippingMethods/index.ts index 289c70d13..c77087d2e 100644 --- a/packages/composables/src/composables/useGetShippingMethods/index.ts +++ b/packages/composables/src/composables/useGetShippingMethods/index.ts @@ -26,7 +26,7 @@ const factoryParams: UseGetShippingMethodsFactory = { const hasAddresses = data.customerCart.shipping_addresses.length > 0; - return hasAddresses ? data.customerCart.shipping_addresses[0].available_shipping_methods : []; + return hasAddresses ? data?.customerCart?.shipping_addresses[0]?.available_shipping_methods : []; }, }; diff --git a/packages/composables/src/composables/useReview/index.ts b/packages/composables/src/composables/useReview/index.ts index c84c4c119..b693f4076 100644 --- a/packages/composables/src/composables/useReview/index.ts +++ b/packages/composables/src/composables/useReview/index.ts @@ -29,7 +29,7 @@ ProductReviewRatingMetadata> = { Logger.debug('[Result]:', { data }); - return data.products.items; + return data?.products?.items ?? []; }, addReview: async (context: Context, params: ComposableFunctionArgs) => { Logger.debug('[Magento] add review params input:', JSON.stringify(params, null, 2)); @@ -42,7 +42,7 @@ ProductReviewRatingMetadata> = { Logger.debug('[Result]:', { data }); - return data.createProductReview.review; + return data?.createProductReview?.review ?? {}; }, loadReviewMetadata: async (context: Context, params) => { Logger.debug('[Magento] load review metadata'); @@ -51,7 +51,7 @@ ProductReviewRatingMetadata> = { Logger.debug('[Result]:', { data }); - return data.productReviewRatingsMetadata.items; + return data?.productReviewRatingsMetadata?.items ?? []; }, loadCustomerReviews: async ( context: Context, @@ -67,7 +67,7 @@ ProductReviewRatingMetadata> = { Logger.debug('[Result]:', { data }); - return data.customer; + return data?.customer ?? {}; }, }; diff --git a/packages/composables/src/composables/useUser/index.ts b/packages/composables/src/composables/useUser/index.ts index 7ab5752f2..00b37a524 100644 --- a/packages/composables/src/composables/useUser/index.ts +++ b/packages/composables/src/composables/useUser/index.ts @@ -46,7 +46,7 @@ CustomerCreateInput Logger.debug('[Result]:', { data }); - return data.customer; + return data?.customer ?? {}; } catch { // eslint-disable-next-line no-void // @ts-ignore @@ -79,11 +79,15 @@ CustomerCreateInput }); } - const { data } = await context.$magento.api.updateCustomer(userData); - + const { data, errors } = await context.$magento.api.updateCustomer(userData); Logger.debug('[Result]:', { data }); - return data.updateCustomerV2.customer; + if (errors) { + throw new Error(errors.map((e) => e.message).join(',')); + } + + // return data.updateCustomerV2.customer; + return data?.updateCustomerV2?.customer || {}; }, register: async (context: Context, params) => { const { diff --git a/packages/composables/src/composables/useUserBilling/index.ts b/packages/composables/src/composables/useUserBilling/index.ts index c4bb3b9c8..ed316d010 100644 --- a/packages/composables/src/composables/useUserBilling/index.ts +++ b/packages/composables/src/composables/useUserBilling/index.ts @@ -22,7 +22,7 @@ const factoryParams: UseUserBillingFactoryParams = { Logger.debug('[Result]:', { data }); - return data.createCustomerAddress; + return data?.createCustomerAddress ?? {}; }, deleteAddress: async (context: Context, params?) => { @@ -32,7 +32,7 @@ const factoryParams: UseUserBillingFactoryParams = { Logger.debug('[Result]:', { data }); - return data.deleteCustomerAddress; + return data?.deleteCustomerAddress ?? {}; }, updateAddress: async (context: Context, params?) => { @@ -42,7 +42,7 @@ const factoryParams: UseUserBillingFactoryParams = { Logger.debug('[Result]:', { data }); - return data.updateCustomerAddress; + return data?.updateCustomerAddress ?? {}; }, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -63,9 +63,8 @@ const factoryParams: UseUserBillingFactoryParams = { Logger.debug('[Result]:', { data }); - return data.updateCustomerAddress; + return data?.updateCustomerAddress ?? {}; }, - }; export default useUserBillingFactory(factoryParams); diff --git a/packages/composables/src/composables/useUserOrder/index.ts b/packages/composables/src/composables/useUserOrder/index.ts index a23aef87c..ac641d67b 100644 --- a/packages/composables/src/composables/useUserOrder/index.ts +++ b/packages/composables/src/composables/useUserOrder/index.ts @@ -26,7 +26,7 @@ const factoryParams: UseUserOrderFactoryParams = { Logger.debug('[Result]:', { data }); - return data.customer.orders; + return data?.customer?.orders ?? {}; }, }; diff --git a/packages/composables/src/composables/useUserShipping/index.ts b/packages/composables/src/composables/useUserShipping/index.ts index 64491b0b7..69d81b707 100644 --- a/packages/composables/src/composables/useUserShipping/index.ts +++ b/packages/composables/src/composables/useUserShipping/index.ts @@ -23,14 +23,14 @@ const factoryParams: UseUserShippingFactoryParams = { Logger.debug('[Result]:', { data }); - return data.createCustomerAddress; + return data?.createCustomerAddress ?? {}; }, deleteAddress: async (context: Context, params) => { Logger.debug('[Magento] delete shipping address', { params }); const { data } = await context.$magento.api.deleteCustomerAddress(params.address.id); - return data.deleteCustomerAddress; + return data?.deleteCustomerAddress ?? {}; }, updateAddress: async (context: Context, params) => { @@ -38,7 +38,7 @@ const factoryParams: UseUserShippingFactoryParams = { const { data } = await context.$magento.api.updateCustomerAddress(transformUserUpdateAddressInput(params)); - return data.updateCustomerAddress; + return data?.updateCustomerAddress ?? {}; }, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -59,7 +59,7 @@ const factoryParams: UseUserShippingFactoryParams = { Logger.debug('[Result]:', { data }); - return data.updateCustomerAddress; + return data?.updateCustomerAddress ?? {}; }, }; diff --git a/packages/composables/src/composables/useWishlist/index.ts b/packages/composables/src/composables/useWishlist/index.ts index 14648ed4a..9656820a9 100644 --- a/packages/composables/src/composables/useWishlist/index.ts +++ b/packages/composables/src/composables/useWishlist/index.ts @@ -25,7 +25,7 @@ const factoryParams: UseWishlistFactoryParams = { Logger.debug('[Result]:', { data }); - return data.customer.wishlists; + return data?.customer?.wishlists ?? []; } return []; @@ -68,7 +68,7 @@ const factoryParams: UseWishlistFactoryParams = { Logger.debug('[Result]:', { data }); - return data.addProductsToWishlist.wishlist; + return data?.addProductsToWishlist?.wishlist ?? {}; case 'ConfigurableProduct': const { data: configurableProductData } = await context.$magento.api.addProductToWishList({ id: '0', @@ -81,7 +81,7 @@ const factoryParams: UseWishlistFactoryParams = { Logger.debug('[Result]:', { data: configurableProductData }); - return configurableProductData.addProductsToWishlist.wishlist; + return configurableProductData?.addProductsToWishlist?.wishlist ?? {}; case 'BundleProduct': const { data: bundleProductData } = await context.$magento.api.addProductToWishList({ id: '0', @@ -94,7 +94,7 @@ const factoryParams: UseWishlistFactoryParams = { Logger.debug('[Result]:', { data: bundleProductData }); - return bundleProductData.addProductsToWishlist.wishlist; + return bundleProductData?.addProductsToWishlist?.wishlist ?? {}; default: // todo implement other options // @ts-ignore @@ -117,7 +117,7 @@ const factoryParams: UseWishlistFactoryParams = { Logger.debug('[Result]:', { data }); - return data.removeProductsFromWishlist.wishlist; + return data?.removeProductsFromWishlist?.wishlist ?? {}; }, clear: async ({ currentWishlist }) => ({}), isInWishlist: (context, params) => { diff --git a/packages/theme/composables/useUiNotification/index.ts b/packages/theme/composables/useUiNotification/index.ts index eab0583dd..620b9d1cc 100644 --- a/packages/theme/composables/useUiNotification/index.ts +++ b/packages/theme/composables/useUiNotification/index.ts @@ -1,4 +1,4 @@ -import { computed, reactive, useContext} from '@nuxtjs/composition-api'; +import { computed, reactive, useContext } from '@nuxtjs/composition-api'; import cookieNames from '~/enums/cookieNameEnum'; interface UiNotification { @@ -56,7 +56,6 @@ const useUiNotification = () => { } }; - if (cookieMessage) { send(cookieMessage); } diff --git a/packages/theme/pages/MyAccount/MyProfile.vue b/packages/theme/pages/MyAccount/MyProfile.vue index c50a342ff..0d472c773 100644 --- a/packages/theme/pages/MyAccount/MyProfile.vue +++ b/packages/theme/pages/MyAccount/MyProfile.vue @@ -55,6 +55,7 @@ extend('min', { extend('password', { message: invalidPasswordMsg, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument validate: (value) => customerPasswordRegExp.test(value), }); @@ -84,8 +85,9 @@ export default defineComponent({ const formHandler = async (fn, onComplete, onError) => { await fn(); - if (error.value.changePassword !== null) { - onError(error.value.changePassword); + const actionErr = error.value.changePassword || error.value.updateUser; + if (actionErr) { + onError(actionErr); } else { onComplete(); } diff --git a/packages/theme/plugins/__tests__/token-expired.spec.js b/packages/theme/plugins/__tests__/token-expired.spec.js index 1c8a868f9..1974440b5 100644 --- a/packages/theme/plugins/__tests__/token-expired.spec.js +++ b/packages/theme/plugins/__tests__/token-expired.spec.js @@ -1,14 +1,25 @@ import tokenExpiredPlugin from '../token-expired'; import cookieNames from '~/enums/cookieNameEnum'; -const callbackResponse = { +const errRes = { data: { - message: 'The current customer isn\'t authorized.', + errors: [ + { + extensions: { + category: 'graphql-authorization', + }, + }, + ], }, }; +const validRes = { + data: { + errors: [], + }, +}; -const appMock = { +const appMockFactory = (callbackResponse) => ({ $vsf: { $magento: { client: { @@ -26,29 +37,34 @@ const appMock = { remove: jest.fn(), set: jest.fn(), }, + router: { + go: jest.fn(), + }, localePath: (t) => t, i18n: { t: (t) => t, }, -}; - -const redirectMock = jest.fn(); +}); describe('Token Expired plugin', () => { beforeEach(() => { jest.resetAllMocks(); }); - it('should work only when the current customer is not authorized', async () => { + it('should be executed only if there is the "graphql-authorization" error', async () => { + const appMock = appMockFactory(validRes); + // eslint-disable-next-line @typescript-eslint/await-thenable - await tokenExpiredPlugin({ app: appMock, redirect: redirectMock }); + await tokenExpiredPlugin({ app: appMock }); - expect(redirectMock).toHaveBeenCalledWith('/'); + expect(appMock.router.go).toHaveBeenCalledTimes(0); }); it('should set message cookie', async () => { + const appMock = appMockFactory(errRes); + // eslint-disable-next-line @typescript-eslint/await-thenable - await tokenExpiredPlugin({ app: appMock, redirect: redirectMock }); + await tokenExpiredPlugin({ app: appMock }); const messageMock = { icon: null, @@ -62,8 +78,10 @@ describe('Token Expired plugin', () => { }); it('should clear customer token and clear cart id', async () => { + const appMock = appMockFactory(errRes); + // eslint-disable-next-line @typescript-eslint/await-thenable - await tokenExpiredPlugin({ app: appMock, redirect: redirectMock }); + await tokenExpiredPlugin({ app: appMock }); expect(appMock.$cookies.remove).toHaveBeenCalledTimes(2); expect(appMock.$cookies.remove).toHaveBeenCalledWith(cookieNames.customerCookieName); diff --git a/packages/theme/plugins/token-expired.js b/packages/theme/plugins/token-expired.js deleted file mode 100644 index d3c8773a1..000000000 --- a/packages/theme/plugins/token-expired.js +++ /dev/null @@ -1,27 +0,0 @@ -import cookieNames from '~/enums/cookieNameEnum'; - -export default ({ app, redirect }) => { - let once = true; - - - app.$vsf.$magento.client.interceptors.response.use(async (r) => { - - if (r.data.message === 'The current customer isn\'t authorized.' && once) { - once = false; - app.$cookies.remove(cookieNames.customerCookieName); - app.$cookies.remove(cookieNames.cartCookieName); - - await app.$cookies.set(cookieNames.messageCookieName, { - message: app.i18n.t('You are not authorized, please log in.'), - type: 'warning', - icon: null, - persist: true, - title: null, - }); - - redirect(app.localePath('/')); - } - - return r; - }); -}; diff --git a/packages/theme/plugins/token-expired.ts b/packages/theme/plugins/token-expired.ts new file mode 100644 index 000000000..b39a15efc --- /dev/null +++ b/packages/theme/plugins/token-expired.ts @@ -0,0 +1,44 @@ +import cookieNames from '~/enums/cookieNameEnum'; + +const hasAuthorizationError = (res): boolean => { + if (!res?.data?.errors) { + return false; + } + + const { errors } = res.data; + + let isAuthErr = false; + + // eslint-disable-next-line no-restricted-syntax + for (const error of errors) { + if (error?.extensions?.category === 'graphql-authorization') { + isAuthErr = true; + break; + } + } + + return isAuthErr; +}; + +export default ({ app }) => { + app.$vsf.$magento.client.interceptors.response.use(async (res): Promise => { + if (!hasAuthorizationError(res)) { + return res; + } + + app.$cookies.remove(cookieNames.customerCookieName); + app.$cookies.remove(cookieNames.cartCookieName); + + await app.$cookies.set(cookieNames.messageCookieName, { + message: app.i18n.t('You are not authorized, please log in.'), + type: 'warning', + icon: null, + persist: true, + title: null, + }); + + app.router.go(app.localePath('/')); + + return false; + }); +};