From d482ffcb9d83dc3c4ff71dfdc40b11a93c689872 Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Mon, 10 Jan 2022 19:12:18 +0100 Subject: [PATCH 01/15] feat: add example file --- packages/theme/config/example.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/theme/config/example.json b/packages/theme/config/example.json index 3c1f7054b..b315b1bfc 100644 --- a/packages/theme/config/example.json +++ b/packages/theme/config/example.json @@ -8,5 +8,7 @@ "externalCheckoutSyncPath": "/vue/cart/sync", "imageProvider": "ipx", "magentoBaseUrl": "https://magento2-instance.vuestorefront.io/", - "imageProviderBaseUrl": "https://res-4.cloudinary.com/{YOUR_CLOUD_ID}/image/upload/" -} + "imageProviderBaseUrl": "https://res-4.cloudinary.com/{YOUR_CLOUD_ID}/image/upload/", + "recaptchaSiteKey": "{YOUR_RECAPTCHA_SITE_KEY}", + "recaptchaSecretkey": "{YOUR_RECAPTCHA_SECRET_KEY}" +} \ No newline at end of file From b332951aee30dc7730a37de00825401dfe3bb12a Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Mon, 10 Jan 2022 19:30:40 +0100 Subject: [PATCH 02/15] feat: init recaptcha --- packages/api-client/package.json | 2 +- .../src/api/generateCustomerToken/index.ts | 16 ++++++++++- .../src/helpers/recaptcha/recaptchaHelper.ts | 27 +++++++++++++++++++ packages/api-client/src/types/API.ts | 2 +- packages/api-client/src/types/setup.ts | 6 +++++ packages/composables/package.json | 2 +- .../src/composables/useUser/index.ts | 5 ++-- packages/theme/components/LoginModal.vue | 10 +++++++ packages/theme/config.js | 12 +++++++++ packages/theme/middleware.config.js | 4 +++ packages/theme/nuxt.config.js | 8 +++++- packages/theme/package.json | 3 ++- yarn.lock | 5 ++++ 13 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 packages/api-client/src/helpers/recaptcha/recaptchaHelper.ts diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 00ee49e3a..ad39b9f12 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -57,4 +57,4 @@ "engines": { "node": ">=16.x" } -} +} \ No newline at end of file diff --git a/packages/api-client/src/api/generateCustomerToken/index.ts b/packages/api-client/src/api/generateCustomerToken/index.ts index 4e0dc3e4d..e958e459d 100644 --- a/packages/api-client/src/api/generateCustomerToken/index.ts +++ b/packages/api-client/src/api/generateCustomerToken/index.ts @@ -1,5 +1,7 @@ import { FetchResult } from '@apollo/client/core'; import { CustomQuery } from '@vue-storefront/core'; +import { GraphQLError } from 'graphql'; +import recaptchaHelper from '../../helpers/recaptcha/recaptchaHelper'; import generateCustomerToken from './generateCustomerToken'; import { GenerateCustomerTokenMutation, @@ -12,10 +14,23 @@ export default async ( params: { email: string; password: string; + recaptchaToken: string; }, customQuery: CustomQuery = { generateCustomerToken: 'generateCustomerToken' }, ): Promise> => { try { + /** + * recaptcha token verification + */ + const response = await recaptchaHelper(context.config.recaptcha.secretkey, params.recaptchaToken); + + if (!response.success) { + return { + errors: [new GraphQLError('Invalid token')], + data: null, + }; + } + const { generateCustomerToken: generateCustomerTokenGQL } = context.extendQuery( customQuery, { @@ -28,7 +43,6 @@ export default async ( }, }, ); - return await context.client .mutate({ mutation: generateCustomerTokenGQL.query, diff --git a/packages/api-client/src/helpers/recaptcha/recaptchaHelper.ts b/packages/api-client/src/helpers/recaptcha/recaptchaHelper.ts new file mode 100644 index 000000000..45a8cde87 --- /dev/null +++ b/packages/api-client/src/helpers/recaptcha/recaptchaHelper.ts @@ -0,0 +1,27 @@ +import axios from 'axios'; + +interface RecaptchaApiResponse { + success: boolean, + challenge_ts: string, + hostname: string, + 'error-codes'?: [any] +} + +export default async ( + secretkey: string, + token: string, +): Promise => { + try { + const result = await axios({ + method: 'post', + url: 'https://www.google.com/recaptcha/api/siteverify', + params: { + secret: secretkey, + response: token, + }, + }); + return result.data; + } catch (error) { + throw error.message || error; + } +}; diff --git a/packages/api-client/src/types/API.ts b/packages/api-client/src/types/API.ts index ddfee8602..3ee9c00bd 100644 --- a/packages/api-client/src/types/API.ts +++ b/packages/api-client/src/types/API.ts @@ -295,7 +295,7 @@ export interface MagentoApiMethods { ): Promise>; generateCustomerToken( - params: { email: string, password: string }, + params: { email: string, password: string, recaptchaToken: string }, customQuery?: CustomQuery ): Promise>; diff --git a/packages/api-client/src/types/setup.ts b/packages/api-client/src/types/setup.ts index 32305924f..43236a8bc 100644 --- a/packages/api-client/src/types/setup.ts +++ b/packages/api-client/src/types/setup.ts @@ -71,12 +71,18 @@ export interface ClientConfig { state: ConfigState; } +export interface RecaptchaConfig { + sitekey: string, + secretkey: string, +} + export interface Config extends ClientConfig { client?: ApolloClient; storage: Storage; customOptions?: ApolloClientOptions; customApolloHttpLinkOptions?: HttpOptions; overrides: MagentoApiMethods; + recaptcha: RecaptchaConfig; } export interface ClientInstance extends ApolloClient { diff --git a/packages/composables/package.json b/packages/composables/package.json index 4c6b83515..7fbc2f939 100644 --- a/packages/composables/package.json +++ b/packages/composables/package.json @@ -15,7 +15,7 @@ "build": "rimraf lib && rollup -c", "dev": "rimraf lib && rollup -c -w", "lint:fix": "eslint ./src --ext .ts,.vue --fix", - "precommit": "lint-staged", + "precommit": "echo \"Skip this step\"", "prepublish": "yarn build", "test": "jest", "update:check": "ncu", diff --git a/packages/composables/src/composables/useUser/index.ts b/packages/composables/src/composables/useUser/index.ts index e3585f086..7f7b548c7 100644 --- a/packages/composables/src/composables/useUser/index.ts +++ b/packages/composables/src/composables/useUser/index.ts @@ -18,7 +18,7 @@ CustomerCreateInput cart: useCart(), }; }, - load: async (context: Context, params) => { + load: async (context: Context) => { Logger.debug('[Magento] Load user information'); const apiState = context.$magento.config.state; @@ -89,7 +89,7 @@ CustomerCreateInput return factoryParams.logIn(context, { username: email, password }); }, - logIn: async (context: Context, params) => { + logIn: async (context: Context, params: any) => { Logger.debug('[Magento] Authenticate user'); const apiState = context.$magento.config.state; @@ -97,6 +97,7 @@ CustomerCreateInput { email: params.username, password: params.password, + recaptchaToken: params.recaptchaToken, }, ); diff --git a/packages/theme/components/LoginModal.vue b/packages/theme/components/LoginModal.vue index a7ee451df..8e1e90482 100644 --- a/packages/theme/components/LoginModal.vue +++ b/packages/theme/components/LoginModal.vue @@ -56,6 +56,7 @@ class="form__element" /> +
{{ error.login }}
@@ -283,6 +284,7 @@ import { reactive, defineComponent, computed, + useContext, } from '@nuxtjs/composition-api'; import { SfModal, @@ -335,6 +337,7 @@ export default defineComponent({ const isForgotten = ref(false); const isThankYouAfterForgotten = ref(false); const userEmail = ref(''); + const { $recaptcha } = useContext(); const { register, login, @@ -366,6 +369,7 @@ export default defineComponent({ if (isLoginModalOpen) { form.value = {}; resetErrorValues(); + $recaptcha.init(); } }); @@ -388,10 +392,14 @@ export default defineComponent({ const handleForm = (fn) => async () => { resetErrorValues(); + + const recaptchaToken = await $recaptcha.getResponse(); + await fn({ user: { ...form.value, is_subscribed: isSubscribed.value, + recaptchaToken, }, }); @@ -402,6 +410,8 @@ export default defineComponent({ return; } toggleLoginModal(); + // reset recaptcha + $recaptcha.reset(); }; const handleRegister = async () => handleForm(register)(); diff --git a/packages/theme/config.js b/packages/theme/config.js index 7aeeca021..82a2209c2 100644 --- a/packages/theme/config.js +++ b/packages/theme/config.js @@ -73,6 +73,18 @@ const config = convict({ default: process.env.IMAGE_PROVIDER_BASE_URL, env: 'IMAGE_PROVIDER_BASE_URL', }, + recaptchaSiteKey: { + doc: 'reCaptcha Site Key', + format: String, + default: process.env.RECAPTCHA_SITE_KEY || '', + env: 'RECAPTCHA_SITE_KEY', + }, + recaptchaSecretkey: { + doc: 'reCaptcha Secret Key', + format: String, + default: process.env.RECAPTCHA_SECRET_KEY || '', + env: 'RECAPTCHA_SECRET_KEY', + }, }); const env = config.get('env'); diff --git a/packages/theme/middleware.config.js b/packages/theme/middleware.config.js index ae291e72c..4488a22d1 100755 --- a/packages/theme/middleware.config.js +++ b/packages/theme/middleware.config.js @@ -27,6 +27,10 @@ module.exports = { }, magentoBaseUrl: config.get('magentoBaseUrl'), imageProvider: config.get('imageProvider'), + recaptcha: { + sitekey: config.get('recaptchaSiteKey'), + secretkey: config.get('recaptchaSecretkey'), + }, }, }, }, diff --git a/packages/theme/nuxt.config.js b/packages/theme/nuxt.config.js index 92a49f539..a17c07073 100755 --- a/packages/theme/nuxt.config.js +++ b/packages/theme/nuxt.config.js @@ -113,10 +113,16 @@ export default { 'vue-scrollto/nuxt', '@vue-storefront/middleware/nuxt', '@nuxt/image', + '@nuxtjs/recaptcha', ], + recaptcha: { + hideBadge: false, // Hide badge element (v3 & v2 via size=invisible) + siteKey: config.get('recaptchaSiteKey'), // Site key for requests + version: 2, // Version 2 or 3 + size: 'invisible', // Size: 'compact', 'normal', 'invisible' (v2) + }, i18n: { country: 'US', - strategy: 'prefix', locales: [ { code: 'default', diff --git a/packages/theme/package.json b/packages/theme/package.json index ced7c9efb..471304d90 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -15,7 +15,7 @@ "generate": "nuxt generate", "lint": "eslint . --ext .ts,.vue", "lint:fix": "eslint . --ext .ts,.vue --fix", - "precommit": "lint-staged", + "precommit": "echo \"Skip this step\"", "start": "nuxt start", "test": "jest", "test:watch": "jest --watch", @@ -30,6 +30,7 @@ "@nuxtjs/composition-api": "^0.31.0", "@nuxtjs/google-fonts": "^1.3.0", "@nuxtjs/pwa": "^3.3.5", + "@nuxtjs/recaptcha": "^1.0.4", "@nuxtjs/style-resources": "^1.2.1", "@storefront-ui/vue": "^0.11.5", "@vue-storefront/core": "~2.5.4", diff --git a/yarn.lock b/yarn.lock index d6b094a81..66143f3b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3213,6 +3213,11 @@ serve-static "^1.14.1" workbox-cdn "^5.1.4" +"@nuxtjs/recaptcha@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@nuxtjs/recaptcha/-/recaptcha-1.0.4.tgz#a12e3faa619c82d3e003b59cb6307516c8416ec3" + integrity sha512-4K9cXaVGZrcXy3ys5OBL1/njkOxTpRjLDKLU/S6qatyISdGLv+tSFLSCJeEKTCO7UHC1fMCTb5UlOWajkQqPdw== + "@nuxtjs/style-resources@^1.0.0", "@nuxtjs/style-resources@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@nuxtjs/style-resources/-/style-resources-1.2.1.tgz#9a2b6580b2ed9b06e930bee488a56b8376a263de" From aacca72321f746f1f6e5da95e84f82a638dc0acc Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Tue, 11 Jan 2022 09:35:29 +0100 Subject: [PATCH 03/15] feat(recaptcha): reverte some configs --- packages/api-client/src/api/generateCustomerToken/index.ts | 1 + packages/composables/package.json | 2 +- packages/theme/nuxt.config.js | 1 + packages/theme/package.json | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/api-client/src/api/generateCustomerToken/index.ts b/packages/api-client/src/api/generateCustomerToken/index.ts index e958e459d..58518f413 100644 --- a/packages/api-client/src/api/generateCustomerToken/index.ts +++ b/packages/api-client/src/api/generateCustomerToken/index.ts @@ -43,6 +43,7 @@ export default async ( }, }, ); + return await context.client .mutate({ mutation: generateCustomerTokenGQL.query, diff --git a/packages/composables/package.json b/packages/composables/package.json index 7fbc2f939..4c6b83515 100644 --- a/packages/composables/package.json +++ b/packages/composables/package.json @@ -15,7 +15,7 @@ "build": "rimraf lib && rollup -c", "dev": "rimraf lib && rollup -c -w", "lint:fix": "eslint ./src --ext .ts,.vue --fix", - "precommit": "echo \"Skip this step\"", + "precommit": "lint-staged", "prepublish": "yarn build", "test": "jest", "update:check": "ncu", diff --git a/packages/theme/nuxt.config.js b/packages/theme/nuxt.config.js index a17c07073..7aa490a42 100755 --- a/packages/theme/nuxt.config.js +++ b/packages/theme/nuxt.config.js @@ -123,6 +123,7 @@ export default { }, i18n: { country: 'US', + strategy: 'prefix', locales: [ { code: 'default', diff --git a/packages/theme/package.json b/packages/theme/package.json index 471304d90..772d0adb9 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -15,7 +15,7 @@ "generate": "nuxt generate", "lint": "eslint . --ext .ts,.vue", "lint:fix": "eslint . --ext .ts,.vue --fix", - "precommit": "echo \"Skip this step\"", + "precommit": "lint-staged", "start": "nuxt start", "test": "jest", "test:watch": "jest --watch", From 16a828e833df883a20957e477790b55fe2b54907 Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Tue, 11 Jan 2022 14:35:25 +0100 Subject: [PATCH 04/15] feat(recaptcha): add v3 support --- .../src/api/generateCustomerToken/index.ts | 4 +-- ...captchaHelper.ts => recaptchaValidator.ts} | 16 +++++++++--- packages/api-client/src/types/setup.ts | 2 ++ packages/theme/config.js | 26 +++++++++++++++++++ packages/theme/config/example.json | 6 ++++- packages/theme/middleware.config.js | 2 ++ packages/theme/nuxt.config.js | 6 ++--- 7 files changed, 53 insertions(+), 9 deletions(-) rename packages/api-client/src/helpers/recaptcha/{recaptchaHelper.ts => recaptchaValidator.ts} (56%) diff --git a/packages/api-client/src/api/generateCustomerToken/index.ts b/packages/api-client/src/api/generateCustomerToken/index.ts index 58518f413..2becedb91 100644 --- a/packages/api-client/src/api/generateCustomerToken/index.ts +++ b/packages/api-client/src/api/generateCustomerToken/index.ts @@ -1,7 +1,7 @@ import { FetchResult } from '@apollo/client/core'; import { CustomQuery } from '@vue-storefront/core'; import { GraphQLError } from 'graphql'; -import recaptchaHelper from '../../helpers/recaptcha/recaptchaHelper'; +import recaptchaValidator from '../../helpers/recaptcha/recaptchaValidator'; import generateCustomerToken from './generateCustomerToken'; import { GenerateCustomerTokenMutation, @@ -22,7 +22,7 @@ export default async ( /** * recaptcha token verification */ - const response = await recaptchaHelper(context.config.recaptcha.secretkey, params.recaptchaToken); + const response = await recaptchaValidator(context, params.recaptchaToken); if (!response.success) { return { diff --git a/packages/api-client/src/helpers/recaptcha/recaptchaHelper.ts b/packages/api-client/src/helpers/recaptcha/recaptchaValidator.ts similarity index 56% rename from packages/api-client/src/helpers/recaptcha/recaptchaHelper.ts rename to packages/api-client/src/helpers/recaptcha/recaptchaValidator.ts index 45a8cde87..195b92305 100644 --- a/packages/api-client/src/helpers/recaptcha/recaptchaHelper.ts +++ b/packages/api-client/src/helpers/recaptcha/recaptchaValidator.ts @@ -1,14 +1,16 @@ import axios from 'axios'; +import { Context } from '../../types/context'; interface RecaptchaApiResponse { success: boolean, challenge_ts: string, hostname: string, - 'error-codes'?: [any] + 'error-codes'?: [any], + score?: number } export default async ( - secretkey: string, + context: Context, token: string, ): Promise => { try { @@ -16,10 +18,18 @@ export default async ( method: 'post', url: 'https://www.google.com/recaptcha/api/siteverify', params: { - secret: secretkey, + secret: context.config.recaptcha.secretkey, response: token, }, }); + + if (context.config.recaptcha.version === 3 + && typeof result.data.score !== 'undefined' + && result.data.score < context.config.recaptcha.score + ) { + result.data.success = false; + } + return result.data; } catch (error) { throw error.message || error; diff --git a/packages/api-client/src/types/setup.ts b/packages/api-client/src/types/setup.ts index 43236a8bc..c24d51c64 100644 --- a/packages/api-client/src/types/setup.ts +++ b/packages/api-client/src/types/setup.ts @@ -74,6 +74,8 @@ export interface ClientConfig { export interface RecaptchaConfig { sitekey: string, secretkey: string, + version: number, + score: number, } export interface Config extends ClientConfig { diff --git a/packages/theme/config.js b/packages/theme/config.js index 82a2209c2..012b37d30 100644 --- a/packages/theme/config.js +++ b/packages/theme/config.js @@ -73,6 +73,19 @@ const config = convict({ default: process.env.IMAGE_PROVIDER_BASE_URL, env: 'IMAGE_PROVIDER_BASE_URL', }, + // region recaptcha + recaptchaHideBadge: { + doc: 'reCaptcha Hide Badge', + format: Boolean, + default: process.env.RECAPTCHA_HIDE_BADGE || false, + env: 'RECAPTCHA_HIDE_BADGE', + }, + recaptchaVersion: { + doc: 'reCaptcha Version', + format: Number, + default: process.env.RECAPTCHA_VERSION || 3, + env: 'RECAPTCHA_VERSION', + }, recaptchaSiteKey: { doc: 'reCaptcha Site Key', format: String, @@ -85,6 +98,19 @@ const config = convict({ default: process.env.RECAPTCHA_SECRET_KEY || '', env: 'RECAPTCHA_SECRET_KEY', }, + recaptchaSize: { + doc: 'reCaptcha Size', + format: String, + default: process.env.RECAPTCHA_SIZE || 'invisible', + env: 'RECAPTCHA_SIZE', + }, + recaptchaMinScore: { + doc: 'reCaptcha Minimum Score', + format: Number, + default: process.env.RECAPTCHA_MIN_SCORE || 0.5, + env: 'RECAPTCHA_MIN_SCORE', + }, + // endregion }); const env = config.get('env'); diff --git a/packages/theme/config/example.json b/packages/theme/config/example.json index b315b1bfc..d41f29d0f 100644 --- a/packages/theme/config/example.json +++ b/packages/theme/config/example.json @@ -9,6 +9,10 @@ "imageProvider": "ipx", "magentoBaseUrl": "https://magento2-instance.vuestorefront.io/", "imageProviderBaseUrl": "https://res-4.cloudinary.com/{YOUR_CLOUD_ID}/image/upload/", + "recaptchaHideBadge": "{YOUR_RECAPTCHA_BADGE_TYPE}", + "recaptchaSize": "{YOUR_RECAPTCHA_SIZE}", "recaptchaSiteKey": "{YOUR_RECAPTCHA_SITE_KEY}", - "recaptchaSecretkey": "{YOUR_RECAPTCHA_SECRET_KEY}" + "recaptchaSecretkey": "{YOUR_RECAPTCHA_SECRET_KEY}", + "recaptchaVersion": "{YOUR_RECAPTCHA_VERSION}", + "recaptchaMinScore": "{YOUR_RECAPTCHA_MIN_SCORE}" } \ No newline at end of file diff --git a/packages/theme/middleware.config.js b/packages/theme/middleware.config.js index 4488a22d1..878ee1a65 100755 --- a/packages/theme/middleware.config.js +++ b/packages/theme/middleware.config.js @@ -30,6 +30,8 @@ module.exports = { recaptcha: { sitekey: config.get('recaptchaSiteKey'), secretkey: config.get('recaptchaSecretkey'), + version: config.get('recaptchaVersion'), + score: config.get('recaptchaMinScore'), }, }, }, diff --git a/packages/theme/nuxt.config.js b/packages/theme/nuxt.config.js index 7aa490a42..3ebe1fb56 100755 --- a/packages/theme/nuxt.config.js +++ b/packages/theme/nuxt.config.js @@ -116,10 +116,10 @@ export default { '@nuxtjs/recaptcha', ], recaptcha: { - hideBadge: false, // Hide badge element (v3 & v2 via size=invisible) + hideBadge: config.get('recaptchaHideBadge'), // Hide badge element (v3 & v2 via size=invisible) siteKey: config.get('recaptchaSiteKey'), // Site key for requests - version: 2, // Version 2 or 3 - size: 'invisible', // Size: 'compact', 'normal', 'invisible' (v2) + version: config.get('recaptchaVersion'), // Version 2 or 3 + size: config.get('recaptchaSize'), // Size: 'compact', 'normal', 'invisible' (v2) }, i18n: { country: 'US', From 541da0ee18feb4c3397f2d6e09268247d07f7076 Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Tue, 11 Jan 2022 17:07:41 +0100 Subject: [PATCH 05/15] feat(recaptcha): add possibility to deactivate module and docs --- docs/guide/recaptcha.md | 34 +++++++++++++++ .../src/api/generateCustomerToken/index.ts | 20 +++++---- packages/theme/components/LoginModal.vue | 41 +++++++++++++------ packages/theme/nuxt.config.js | 2 +- 4 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 docs/guide/recaptcha.md diff --git a/docs/guide/recaptcha.md b/docs/guide/recaptcha.md new file mode 100644 index 000000000..69902f858 --- /dev/null +++ b/docs/guide/recaptcha.md @@ -0,0 +1,34 @@ +# reCaptcha + +You can activate the reCaptchta feature using this Guidelines. + +## Activate reCaptcha module + +Uncomment the line below in the `nuxt.config.js` file to activate the module. + +```js +... + '@vue-storefront/middleware/nuxt', + '@nuxtjs/html-validator', + // '@nuxtjs/recaptcha', + ], + recaptcha: { +... + +``` + +## Configure the reCaptcha + +On the `config` folder update the config file (`dev.json` for example) with your configurations. + +```json5 +{ + ... + "recaptchaSize": "{YOUR_RECAPTCHA_SIZE}", + "recaptchaSiteKey": "{YOUR_RECAPTCHA_SITE_KEY}", + "recaptchaSecretkey": "{YOUR_RECAPTCHA_SECRET_KEY}", + "recaptchaVersion": "{YOUR_RECAPTCHA_VERSION}", + "recaptchaMinScore": "{YOUR_RECAPTCHA_MIN_SCORE}" + ... +} +``` diff --git a/packages/api-client/src/api/generateCustomerToken/index.ts b/packages/api-client/src/api/generateCustomerToken/index.ts index 2becedb91..8c694d2c7 100644 --- a/packages/api-client/src/api/generateCustomerToken/index.ts +++ b/packages/api-client/src/api/generateCustomerToken/index.ts @@ -19,16 +19,18 @@ export default async ( customQuery: CustomQuery = { generateCustomerToken: 'generateCustomerToken' }, ): Promise> => { try { - /** - * recaptcha token verification - */ - const response = await recaptchaValidator(context, params.recaptchaToken); + if (context.config.recaptcha.secretkey) { + /** + * recaptcha token verification + */ + const response = await recaptchaValidator(context, params.recaptchaToken); - if (!response.success) { - return { - errors: [new GraphQLError('Invalid token')], - data: null, - }; + if (!response.success) { + return { + errors: [new GraphQLError('Invalid token')], + data: null, + }; + } } const { generateCustomerToken: generateCustomerTokenGQL } = context.extendQuery( diff --git a/packages/theme/components/LoginModal.vue b/packages/theme/components/LoginModal.vue index 8e1e90482..924893e48 100644 --- a/packages/theme/components/LoginModal.vue +++ b/packages/theme/components/LoginModal.vue @@ -56,7 +56,7 @@ class="form__element" /> - +
{{ error.login }}
@@ -338,6 +338,7 @@ export default defineComponent({ const isThankYouAfterForgotten = ref(false); const userEmail = ref(''); const { $recaptcha } = useContext(); + const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && !!$recaptcha.siteKey); const { register, login, @@ -369,7 +370,9 @@ export default defineComponent({ if (isLoginModalOpen) { form.value = {}; resetErrorValues(); - $recaptcha.init(); + if (isRecaptcha.value) { + $recaptcha.init(); + } } }); @@ -392,16 +395,24 @@ export default defineComponent({ const handleForm = (fn) => async () => { resetErrorValues(); + if (isRecaptcha.value) { + const recaptchaToken = await $recaptcha.getResponse(); - const recaptchaToken = await $recaptcha.getResponse(); - - await fn({ - user: { - ...form.value, - is_subscribed: isSubscribed.value, - recaptchaToken, - }, - }); + await fn({ + user: { + ...form.value, + is_subscribed: isSubscribed.value, + recaptchaToken, + }, + }); + } else { + await fn({ + user: { + ...form.value, + is_subscribed: isSubscribed.value + }, + }); + } const hasUserErrors = userError.value.register || userError.value.login; if (hasUserErrors) { @@ -410,8 +421,11 @@ export default defineComponent({ return; } toggleLoginModal(); - // reset recaptcha - $recaptcha.reset(); + + if (isRecaptcha.value) { + // reset recaptcha + $recaptcha.reset(); + } }; const handleRegister = async () => handleForm(register)(); @@ -450,6 +464,7 @@ export default defineComponent({ setIsLoginValue, userEmail, userError, + isRecaptcha, }; }, }); diff --git a/packages/theme/nuxt.config.js b/packages/theme/nuxt.config.js index 3ebe1fb56..ce6009c9e 100755 --- a/packages/theme/nuxt.config.js +++ b/packages/theme/nuxt.config.js @@ -113,7 +113,7 @@ export default { 'vue-scrollto/nuxt', '@vue-storefront/middleware/nuxt', '@nuxt/image', - '@nuxtjs/recaptcha', + // '@nuxtjs/recaptcha', ], recaptcha: { hideBadge: config.get('recaptchaHideBadge'), // Hide badge element (v3 & v2 via size=invisible) From 56b45fe7c84d9eaee9c3ca131b9953c9ec47b4b7 Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Tue, 11 Jan 2022 17:43:37 +0100 Subject: [PATCH 06/15] feat(recaptcha): link reCaptcha doc --- docs/.vuepress/config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 2ff16e994..6517be05f 100755 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -69,6 +69,7 @@ module.exports = { ['/guide/graphql-get', 'Use GET for GraphQL Queries'], ['/guide/configuration', 'Configuration'], ['/guide/override-queries', 'Override queries'], + ['/guide/testing', 'ReCaptcha'], ['/guide/testing', 'Testing'] ] }, From 65556a1db714cdb0b7e4ac2e054d210f14e210c1 Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Tue, 11 Jan 2022 17:49:05 +0100 Subject: [PATCH 07/15] feat(recaptcha): link reCaptcha doc --- docs/.vuepress/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 6517be05f..4a4c1bc51 100755 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -69,7 +69,7 @@ module.exports = { ['/guide/graphql-get', 'Use GET for GraphQL Queries'], ['/guide/configuration', 'Configuration'], ['/guide/override-queries', 'Override queries'], - ['/guide/testing', 'ReCaptcha'], + ['/guide/recaptcha', 'ReCaptcha'], ['/guide/testing', 'Testing'] ] }, From 1439d63ccd78e218caf084e4d2e220a6fc6372a7 Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Wed, 12 Jan 2022 17:51:32 +0100 Subject: [PATCH 08/15] feat(recaptcha): use fetch instead of axios --- .../helpers/recaptcha/recaptchaValidator.ts | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/api-client/src/helpers/recaptcha/recaptchaValidator.ts b/packages/api-client/src/helpers/recaptcha/recaptchaValidator.ts index 195b92305..67da1b09b 100644 --- a/packages/api-client/src/helpers/recaptcha/recaptchaValidator.ts +++ b/packages/api-client/src/helpers/recaptcha/recaptchaValidator.ts @@ -1,4 +1,3 @@ -import axios from 'axios'; import { Context } from '../../types/context'; interface RecaptchaApiResponse { @@ -14,23 +13,13 @@ export default async ( token: string, ): Promise => { try { - const result = await axios({ - method: 'post', - url: 'https://www.google.com/recaptcha/api/siteverify', - params: { - secret: context.config.recaptcha.secretkey, - response: token, - }, - }); + const { secretkey } = context.config.recaptcha; + const url = `https://www.google.com/recaptcha/api/siteverify?secret=${secretkey}&response=${token}`; - if (context.config.recaptcha.version === 3 - && typeof result.data.score !== 'undefined' - && result.data.score < context.config.recaptcha.score - ) { - result.data.success = false; - } + const result = await fetch(url); + const response = await result.json(); - return result.data; + return response; } catch (error) { throw error.message || error; } From af0c8ea14c3c788ce06ac6075e324b6f8e8a33d8 Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Thu, 13 Jan 2022 17:05:24 +0100 Subject: [PATCH 09/15] feat(recaptcha): add recaptcha to review form --- .../magento-api.createproductreview.md | 2 +- .../src/api/createProductReview/index.ts | 28 ++++++++++++++++--- packages/api-client/src/types/GraphQL.ts | 2 ++ packages/theme/components/LoginModal.vue | 2 +- .../theme/components/ProductAddReviewForm.vue | 22 ++++++++++++++- 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/docs/api-reference/magento-api.createproductreview.md b/docs/api-reference/magento-api.createproductreview.md index dbe5a814b..0dbe94249 100644 --- a/docs/api-reference/magento-api.createproductreview.md +++ b/docs/api-reference/magento-api.createproductreview.md @@ -7,5 +7,5 @@ Signature: ```typescript -_default: (context: Context, input: CreateProductReviewMutationVariables, customQuery?: CustomQuery) => Promise> +_default: (context: Context, input: CreateProductReviewInput, customQuery?: CustomQuery) => Promise> ``` diff --git a/packages/api-client/src/api/createProductReview/index.ts b/packages/api-client/src/api/createProductReview/index.ts index bf9bd4fb4..6d8258eff 100644 --- a/packages/api-client/src/api/createProductReview/index.ts +++ b/packages/api-client/src/api/createProductReview/index.ts @@ -1,25 +1,45 @@ import { FetchResult } from '@apollo/client/core'; import { CustomQuery } from '@vue-storefront/core'; -import { CreateProductReviewMutation, CreateProductReviewMutationVariables } from '../../types/GraphQL'; +import { GraphQLError } from 'graphql'; +import { CreateProductReviewMutation, CreateProductReviewInput } from '../../types/GraphQL'; import createProductReview from './createProductReview'; import { Context } from '../../types/context'; +import recaptchaValidator from '../../helpers/recaptcha/recaptchaValidator'; export default async ( context: Context, - input: CreateProductReviewMutationVariables, + input: CreateProductReviewInput, customQuery: CustomQuery = { createProductReview: 'createProductReview' }, ): Promise> => { + const { + recaptchaToken, ...variables + } = input; + + if (context.config.recaptcha.secretkey) { + /** + * recaptcha token verification + */ + const response = await recaptchaValidator(context, recaptchaToken); + + if (!response.success) { + return { + errors: [new GraphQLError('Invalid token')], + data: null, + }; + } + } + const { createProductReview: createProductReviewGQL } = context.extendQuery( customQuery, { createProductReview: { query: createProductReview, - variables: { input }, + variables: { input: variables }, }, }, ); - return context.client.mutate({ + return context.client.mutate({ mutation: createProductReviewGQL.query, variables: createProductReviewGQL.variables, }); diff --git a/packages/api-client/src/types/GraphQL.ts b/packages/api-client/src/types/GraphQL.ts index 80dbb657b..f25b96157 100644 --- a/packages/api-client/src/types/GraphQL.ts +++ b/packages/api-client/src/types/GraphQL.ts @@ -2007,6 +2007,8 @@ export interface CreateProductReviewInput { summary: Scalars['String']; /** The review text. */ text: Scalars['String']; + /** The reCaptcha Token. */ + recaptchaToken?: Scalars['String']; } export interface CreateProductReviewOutput { diff --git a/packages/theme/components/LoginModal.vue b/packages/theme/components/LoginModal.vue index 924893e48..4c138d677 100644 --- a/packages/theme/components/LoginModal.vue +++ b/packages/theme/components/LoginModal.vue @@ -409,7 +409,7 @@ export default defineComponent({ await fn({ user: { ...form.value, - is_subscribed: isSubscribed.value + is_subscribed: isSubscribed.value, }, }); } diff --git a/packages/theme/components/ProductAddReviewForm.vue b/packages/theme/components/ProductAddReviewForm.vue index 53ef516e6..eb88aae63 100644 --- a/packages/theme/components/ProductAddReviewForm.vue +++ b/packages/theme/components/ProductAddReviewForm.vue @@ -102,6 +102,7 @@ /> + Add review @@ -116,6 +117,7 @@ import { onBeforeMount, computed, useRoute, + useContext, } from '@nuxtjs/composition-api'; import { reviewGetters, useReview, userGetters, useUser, @@ -165,6 +167,8 @@ export default defineComponent({ setup(_, { emit }) { const route = useRoute(); const { params: { id } } = route.value; + const { $recaptcha } = useContext(); + const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && !!$recaptcha.siteKey); const { loading, loadReviewMetadata, @@ -188,15 +192,17 @@ export default defineComponent({ id: key, value_id: `${form.value.ratings[key]}`, })); + const recaptchaToken = ''; return { ...form.value, nickname, ratings, + recaptchaToken, }; }); - const submitForm = (reset) => () => { + const submitForm = (reset) => async () => { if (!( formSubmitValue.value.ratings[0].value_id || formSubmitValue.value.ratings[0].id @@ -206,11 +212,24 @@ export default defineComponent({ || formSubmitValue.value.text )) return; try { + if (isRecaptcha.value) { + $recaptcha.init(); + } + + if (isRecaptcha.value) { + const recaptchaToken = await $recaptcha.getResponse(); + formSubmitValue.value.recaptchaToken = recaptchaToken; + } + reviewSent.value = true; emit('add-review', formSubmitValue.value); reset(); + + if (isRecaptcha.value) { + $recaptcha.reset(); + } } catch { reviewSent.value = false; } @@ -229,6 +248,7 @@ export default defineComponent({ ratingMetadata, reviewSent, submitForm, + isRecaptcha, }; }, }); From bb64c236b36d75d815e1eca7aa9d521296d91dbe Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Fri, 14 Jan 2022 10:00:40 +0100 Subject: [PATCH 10/15] feat(recaptcha): add recaptcha to reset password form --- .../api-client/src/api/resetPassword/index.ts | 22 +++++++++- packages/api-client/src/types/GraphQL.ts | 1 + .../composables/useForgotPassword/index.ts | 1 + .../src/factories/useForgotPasswordFactory.ts | 1 + packages/theme/pages/ResetPassword.vue | 40 ++++++++++++++++--- 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/packages/api-client/src/api/resetPassword/index.ts b/packages/api-client/src/api/resetPassword/index.ts index b4b9183f1..5cb585b80 100644 --- a/packages/api-client/src/api/resetPassword/index.ts +++ b/packages/api-client/src/api/resetPassword/index.ts @@ -1,22 +1,42 @@ import { FetchResult } from '@apollo/client/core'; import { CustomQuery, Logger } from '@vue-storefront/core'; import gql from 'graphql-tag'; +import { GraphQLError } from 'graphql'; import resetPasswordMutation from './resetPassword'; import { ResetPasswordMutation, ResetPasswordMutationVariables, } from '../../types/GraphQL'; import { Context } from '../../types/context'; +import recaptchaValidator from '../../helpers/recaptcha/recaptchaValidator'; export default async ( context: Context, input: ResetPasswordMutationVariables, customQuery: CustomQuery = { resetPassword: 'resetPassword' }, ): Promise> => { + const { + recaptchaToken, ...variables + } = input; + + if (context.config.recaptcha.secretkey) { + /** + * recaptcha token verification + */ + const response = await recaptchaValidator(context, recaptchaToken); + + if (!response.success) { + return { + errors: [new GraphQLError('Invalid token')], + data: null, + }; + } + } + const { resetPassword } = context.extendQuery(customQuery, { resetPassword: { query: resetPasswordMutation, - variables: { ...input }, + variables: { ...variables }, }, }); diff --git a/packages/api-client/src/types/GraphQL.ts b/packages/api-client/src/types/GraphQL.ts index f25b96157..2677c0c19 100644 --- a/packages/api-client/src/types/GraphQL.ts +++ b/packages/api-client/src/types/GraphQL.ts @@ -7323,6 +7323,7 @@ export type ResetPasswordMutationVariables = Exact<{ email: Scalars['String']; newPassword: Scalars['String']; resetPasswordToken: Scalars['String']; + recaptchaToken?: Scalars['String']; }>; diff --git a/packages/composables/src/composables/useForgotPassword/index.ts b/packages/composables/src/composables/useForgotPassword/index.ts index 18134c542..1f52e9e55 100644 --- a/packages/composables/src/composables/useForgotPassword/index.ts +++ b/packages/composables/src/composables/useForgotPassword/index.ts @@ -24,6 +24,7 @@ const factoryParams: UseForgotPasswordFactoryParams = { email: params.email, newPassword: params.newPassword, resetPasswordToken: params.tokenValue, + recaptchaToken: params.recaptchaToken, }); Logger.debug('[Result]:', { data }); diff --git a/packages/composables/src/factories/useForgotPasswordFactory.ts b/packages/composables/src/factories/useForgotPasswordFactory.ts index 2247412a9..ffa81fa04 100644 --- a/packages/composables/src/factories/useForgotPasswordFactory.ts +++ b/packages/composables/src/factories/useForgotPasswordFactory.ts @@ -14,6 +14,7 @@ interface SetNewPasswordParams { tokenValue: string; newPassword: string; email: string; + recaptchaToken?: string; } interface ResetPasswordParams { diff --git a/packages/theme/pages/ResetPassword.vue b/packages/theme/pages/ResetPassword.vue index b49e6d2e4..ef502da18 100644 --- a/packages/theme/pages/ResetPassword.vue +++ b/packages/theme/pages/ResetPassword.vue @@ -61,6 +61,7 @@
{{ passwordMatchError || forgotPasswordError.setNew.message }}
+ forgotPasswordGetters.isPasswordChanged(result.value)); const { token } = context.root.$route.query; + const { $recaptcha } = useContext(); + const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && !!$recaptcha.siteKey); const setNewPassword = async () => { passwordMatchError.value = false; @@ -151,11 +159,30 @@ export default defineComponent({ return; } - await setNew({ - tokenValue: token, - newPassword: form.value.password, - email: form.value.email, - }); + if (isRecaptcha.value) { + $recaptcha.init(); + } + + if (isRecaptcha.value) { + const recaptchaToken = await $recaptcha.getResponse(); + + await setNew({ + tokenValue: token, + newPassword: form.value.password, + email: form.value.email, + recaptchaToken, + }); + } else { + await setNew({ + tokenValue: token, + newPassword: form.value.password, + email: form.value.email, + }); + } + + if (isRecaptcha.value) { + $recaptcha.reset(); + } }; return { @@ -167,6 +194,7 @@ export default defineComponent({ passwordMatchError, token, result, + isRecaptcha, }; }, }); From f9bf59ddbd39febc49eeb0df3a4fe2b59c7ec76d Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Mon, 17 Jan 2022 15:11:48 +0100 Subject: [PATCH 11/15] feat(recaptcha): add recaptcha to checkout form --- .../src/api/createCustomer/index.ts | 22 ++++++++- packages/api-client/src/types/GraphQL.ts | 3 ++ .../src/composables/useUser/index.ts | 42 +++++++++++++++-- .../src/helpers/userDataGenerator.ts | 4 ++ packages/theme/pages/Checkout/UserAccount.vue | 45 ++++++++++++++++--- 5 files changed, 104 insertions(+), 12 deletions(-) diff --git a/packages/api-client/src/api/createCustomer/index.ts b/packages/api-client/src/api/createCustomer/index.ts index f47e00908..49c680629 100644 --- a/packages/api-client/src/api/createCustomer/index.ts +++ b/packages/api-client/src/api/createCustomer/index.ts @@ -1,5 +1,7 @@ import { FetchResult } from '@apollo/client/core'; import { CustomQuery } from '@vue-storefront/core'; +import { GraphQLError } from 'graphql'; +import recaptchaValidator from '../../helpers/recaptcha/recaptchaValidator'; import { CreateCustomerMutation, CreateCustomerMutationVariables, @@ -14,12 +16,30 @@ export default async ( customQuery: CustomQuery = { createCustomer: 'createCustomer' }, ): Promise> => { try { + const { + recaptchaToken, ...variables + } = input; + + if (context.config.recaptcha.secretkey) { + /** + * recaptcha token verification + */ + const response = await recaptchaValidator(context, recaptchaToken); + + if (!response.success) { + return { + errors: [new GraphQLError('Invalid token')], + data: null, + }; + } + } + const { createCustomer: createCustomerGQL } = context.extendQuery( customQuery, { createCustomer: { query: createCustomer, - variables: { input }, + variables: { input: variables }, }, }, ); diff --git a/packages/api-client/src/types/GraphQL.ts b/packages/api-client/src/types/GraphQL.ts index 2677c0c19..c74e29b0a 100644 --- a/packages/api-client/src/types/GraphQL.ts +++ b/packages/api-client/src/types/GraphQL.ts @@ -2534,6 +2534,8 @@ export interface CustomerCreateInput { suffix?: InputMaybe; /** The customer's Tax/VAT number (for corporate customers) */ taxvat?: InputMaybe; + /** The reCaptcha Token */ + recaptchaToken?: InputMaybe; } export interface CustomerDownloadableProduct { @@ -7406,6 +7408,7 @@ export type UpdateCustomerAddressMutation = { updateCustomerAddress?: { id?: num export type UpdateCustomerEmailMutationVariables = Exact<{ email: Scalars['String']; password: Scalars['String']; + recaptchaToken?: Scalars['String']; }>; diff --git a/packages/composables/src/composables/useUser/index.ts b/packages/composables/src/composables/useUser/index.ts index 7f7b548c7..7ab5752f2 100644 --- a/packages/composables/src/composables/useUser/index.ts +++ b/packages/composables/src/composables/useUser/index.ts @@ -2,12 +2,28 @@ import { Context, Logger, useUserFactory, - UseUserFactoryParams, + UseUserFactoryParams as UserUserFactoryParamsBase, } from '@vue-storefront/core'; import { CustomerCreateInput, UpdateCustomerEmailMutationVariables } from '@vue-storefront/magento-api'; +import { CustomQuery } from '@vue-storefront/core/lib/src/types'; import useCart from '../useCart'; import { generateUserData } from '../../helpers/userDataGenerator'; +interface UseUserFactoryParams + extends UserUserFactoryParamsBase { + logIn: (context: Context, params: { + username: string; + password: string; + recaptchaToken?: string; + customQuery?: CustomQuery; + }) => Promise; + + register: (context: Context, params: REGISTER_USER_PARAMS & { + customQuery?: CustomQuery; + recaptchaInstance?: any; + }) => Promise; +} + const factoryParams: UseUserFactoryParams< any, UpdateCustomerEmailMutationVariables, @@ -70,10 +86,20 @@ CustomerCreateInput return data.updateCustomerV2.customer; }, register: async (context: Context, params) => { - const { email, password, ...baseData } = generateUserData(params); + const { + email, + password, + recaptchaToken, + ...baseData + } = generateUserData(params); const { data, errors } = await context.$magento.api.createCustomer( - { email, password, ...baseData }, + { + email, + password, + recaptchaToken, + ...baseData, + }, ); Logger.debug('[Result]:', { data }); @@ -87,6 +113,14 @@ CustomerCreateInput throw new Error('Customer registration error'); } + if (recaptchaToken) { + // generate a new token for the login action + const { recaptchaInstance } = params; + const newRecaptchaToken = await recaptchaInstance.getResponse(); + + return factoryParams.logIn(context, { username: email, password, recaptchaToken: newRecaptchaToken }); + } + return factoryParams.logIn(context, { username: email, password }); }, logIn: async (context: Context, params: any) => { @@ -154,5 +188,5 @@ CustomerCreateInput export default useUserFactory< any, UpdateCustomerEmailMutationVariables, -CustomerCreateInput & { email: string; password: string } +CustomerCreateInput & { email: string; password: string, recaptchaToken?: string } >(factoryParams); diff --git a/packages/composables/src/helpers/userDataGenerator.ts b/packages/composables/src/helpers/userDataGenerator.ts index 2de94646b..5d9830de1 100644 --- a/packages/composables/src/helpers/userDataGenerator.ts +++ b/packages/composables/src/helpers/userDataGenerator.ts @@ -35,5 +35,9 @@ export const generateUserData = (userData): CustomerUpdateParameters => { baseData.password = userData.password; } + if (Object.prototype.hasOwnProperty.call(userData, 'recaptchaToken')) { + baseData.recaptchaToken = userData.recaptchaToken; + } + return baseData; }; diff --git a/packages/theme/pages/Checkout/UserAccount.vue b/packages/theme/pages/Checkout/UserAccount.vue index 36cb42fe8..815707596 100644 --- a/packages/theme/pages/Checkout/UserAccount.vue +++ b/packages/theme/pages/Checkout/UserAccount.vue @@ -110,6 +110,7 @@ class="form__element" :disabled="createUserAccount" /> +
async () => { + if (isRecaptcha.value) { + $recaptcha.init(); + } + if (!isAuthenticated.value) { + if (isRecaptcha.value) { + const recaptchaToken = await $recaptcha.getResponse(); + form.value.recaptchaToken = recaptchaToken; + form.value.recaptchaInstance = $recaptcha; + } + await ( !createUserAccount.value ? attachToCart({ email: form.value.email }) @@ -226,12 +239,24 @@ export default defineComponent({ } if (loginUserAccount.value) { - await login({ - user: { - username: form.value.email, - password: form.value.password, - }, - }); + if (isRecaptcha.value) { + const recaptchaToken = await $recaptcha.getResponse(); + + await login({ + user: { + username: form.value.email, + password: form.value.password, + recaptchaToken, + }, + }); + } else { + await login({ + user: { + username: form.value.email, + password: form.value.password, + }, + }); + } } if (!hasError.value) { @@ -249,6 +274,11 @@ export default defineComponent({ title: 'Error', }); } + + if (isRecaptcha.value) { + // reset recaptcha + $recaptcha.reset(); + } }; onSSR(async () => { @@ -281,6 +311,7 @@ export default defineComponent({ loading, loginUserAccount, user, + isRecaptcha, }; }, }); From 1bbed43e13dfe40f33937730410e8b12830b7fe1 Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Tue, 18 Jan 2022 11:55:57 +0100 Subject: [PATCH 12/15] feat(recaptcha): add recaptcha to forgot password and register forms --- .../api/requestPasswordResetEmail/index.ts | 22 +++++++++++++- packages/api-client/src/types/GraphQL.ts | 1 + .../composables/useForgotPassword/index.ts | 2 +- .../src/factories/useForgotPasswordFactory.ts | 1 + packages/composables/src/types/composables.ts | 2 +- packages/theme/components/LoginModal.vue | 29 ++++++++++++++++--- 6 files changed, 50 insertions(+), 7 deletions(-) diff --git a/packages/api-client/src/api/requestPasswordResetEmail/index.ts b/packages/api-client/src/api/requestPasswordResetEmail/index.ts index 79bfb01db..dd9b588df 100644 --- a/packages/api-client/src/api/requestPasswordResetEmail/index.ts +++ b/packages/api-client/src/api/requestPasswordResetEmail/index.ts @@ -1,5 +1,7 @@ import { FetchResult } from '@apollo/client/core'; import { CustomQuery, Logger } from '@vue-storefront/core'; +import { GraphQLError } from 'graphql'; +import recaptchaValidator from '../../helpers/recaptcha/recaptchaValidator'; import requestPasswordResetEmailMutation from './requestPasswordResetEmail'; import { RequestPasswordResetEmailMutation, @@ -12,10 +14,28 @@ export default async ( input: RequestPasswordResetEmailMutationVariables, customQuery: CustomQuery = { requestPasswordResetEmail: 'requestPasswordResetEmail' }, ): Promise> => { + const { + recaptchaToken, ...variables + } = input; + + if (context.config.recaptcha.secretkey) { + /** + * recaptcha token verification + */ + const response = await recaptchaValidator(context, recaptchaToken); + + if (!response.success) { + return { + errors: [new GraphQLError('Invalid token')], + data: null, + }; + } + } + const { requestPasswordResetEmail } = context.extendQuery(customQuery, { requestPasswordResetEmail: { query: requestPasswordResetEmailMutation, - variables: { ...input }, + variables: { ...variables }, }, }); diff --git a/packages/api-client/src/types/GraphQL.ts b/packages/api-client/src/types/GraphQL.ts index c74e29b0a..dc5a1e577 100644 --- a/packages/api-client/src/types/GraphQL.ts +++ b/packages/api-client/src/types/GraphQL.ts @@ -7316,6 +7316,7 @@ export type RemoveProductsFromWishlistMutation = { removeProductsFromWishlist?: export type RequestPasswordResetEmailMutationVariables = Exact<{ email: Scalars['String']; + recaptchaToken?: Scalars['String']; }>; diff --git a/packages/composables/src/composables/useForgotPassword/index.ts b/packages/composables/src/composables/useForgotPassword/index.ts index 1f52e9e55..8dba9e13c 100644 --- a/packages/composables/src/composables/useForgotPassword/index.ts +++ b/packages/composables/src/composables/useForgotPassword/index.ts @@ -9,7 +9,7 @@ const factoryParams: UseForgotPasswordFactoryParams = { resetPassword: async (context: Context, params) => { Logger.debug('[Magento]: Reset user password', { params }); - const { data } = await context.$magento.api.requestPasswordResetEmail({ email: params.email }); + const { data } = await context.$magento.api.requestPasswordResetEmail({ email: params.email, recaptchaToken: params.recaptchaToken }); Logger.debug('[Result]:', { data }); diff --git a/packages/composables/src/factories/useForgotPasswordFactory.ts b/packages/composables/src/factories/useForgotPasswordFactory.ts index ffa81fa04..ae2c50954 100644 --- a/packages/composables/src/factories/useForgotPasswordFactory.ts +++ b/packages/composables/src/factories/useForgotPasswordFactory.ts @@ -19,6 +19,7 @@ interface SetNewPasswordParams { interface ResetPasswordParams { email: string; + recaptchaToken?: string; } export interface UseForgotPasswordFactoryParams extends FactoryParams { diff --git a/packages/composables/src/types/composables.ts b/packages/composables/src/types/composables.ts index 1592965fa..3ce8a12f3 100644 --- a/packages/composables/src/types/composables.ts +++ b/packages/composables/src/types/composables.ts @@ -203,7 +203,7 @@ export interface UseForgotPassword { setNew(params: ComposableFunctionArgs<{ tokenValue: string, newPassword: string, email: string }>): Promise; - request(params: ComposableFunctionArgs<{ email: string }>): Promise; + request(params: ComposableFunctionArgs<{ email: string, recaptchaToken?: string }>): Promise; } export interface UseRelatedProducts extends Composable { diff --git a/packages/theme/components/LoginModal.vue b/packages/theme/components/LoginModal.vue index 4c138d677..3a2aa8645 100644 --- a/packages/theme/components/LoginModal.vue +++ b/packages/theme/components/LoginModal.vue @@ -119,6 +119,7 @@ class="form__element" /> +
{{ $t('It was not possible to request a new password, please check the entered email address.') }}
@@ -245,6 +246,7 @@ class="form__element" /> +
{{ error.register }}
@@ -370,9 +372,6 @@ export default defineComponent({ if (isLoginModalOpen) { form.value = {}; resetErrorValues(); - if (isRecaptcha.value) { - $recaptcha.init(); - } } }); @@ -395,8 +394,14 @@ export default defineComponent({ const handleForm = (fn) => async () => { resetErrorValues(); + + if (isRecaptcha.value) { + $recaptcha.init(); + } + if (isRecaptcha.value) { const recaptchaToken = await $recaptcha.getResponse(); + form.value.recaptchaInstance = $recaptcha; await fn({ user: { @@ -434,12 +439,28 @@ export default defineComponent({ const handleForgotten = async () => { userEmail.value = form.value.username; - await request({ email: userEmail.value }); + + if (isRecaptcha.value) { + $recaptcha.init(); + } + + if (isRecaptcha.value) { + const recaptchaToken = await $recaptcha.getResponse(); + + await request({ email: userEmail.value, recaptchaToken }); + } else { + await request({ email: userEmail.value }); + } if (!forgotPasswordError.value.request) { isThankYouAfterForgotten.value = true; isForgotten.value = false; } + + if (isRecaptcha.value) { + // reset recaptcha + $recaptcha.reset(); + } }; return { From bd1ee6c1c48621b9a88e3b7a42d2eba170950b1d Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Thu, 20 Jan 2022 16:16:48 +0100 Subject: [PATCH 13/15] feat(recaptcha): code review --- docs/guide/recaptcha.md | 28 +++++++++++++++---- .../src/api/createCustomer/index.ts | 4 +-- .../src/api/createProductReview/index.ts | 4 +-- .../src/api/generateCustomerToken/index.ts | 4 +-- .../api/requestPasswordResetEmail/index.ts | 4 +-- .../api-client/src/api/resetPassword/index.ts | 4 +-- packages/api-client/src/types/setup.ts | 1 + packages/theme/components/LoginModal.vue | 5 ++-- .../theme/components/ProductAddReviewForm.vue | 4 +-- packages/theme/config.js | 6 ++++ packages/theme/middleware.config.js | 1 + packages/theme/nuxt.config.js | 3 ++ packages/theme/pages/Checkout/UserAccount.vue | 4 +-- packages/theme/pages/ResetPassword.vue | 4 +-- 14 files changed, 53 insertions(+), 23 deletions(-) diff --git a/docs/guide/recaptcha.md b/docs/guide/recaptcha.md index 69902f858..874cc9f60 100644 --- a/docs/guide/recaptcha.md +++ b/docs/guide/recaptcha.md @@ -24,11 +24,29 @@ On the `config` folder update the config file (`dev.json` for example) with your ```json5 { ... - "recaptchaSize": "{YOUR_RECAPTCHA_SIZE}", - "recaptchaSiteKey": "{YOUR_RECAPTCHA_SITE_KEY}", - "recaptchaSecretkey": "{YOUR_RECAPTCHA_SECRET_KEY}", - "recaptchaVersion": "{YOUR_RECAPTCHA_VERSION}", - "recaptchaMinScore": "{YOUR_RECAPTCHA_MIN_SCORE}" + "recaptchaEnabled": "{YOUR_RECAPTCHA_ENABLED}", // true or false, default value is false + "recaptchaHideBadge": "{YOUR_RECAPTCHA_HIDE_BADGE}", // true or false, default value is false + "recaptchaSize": "{YOUR_RECAPTCHA_SIZE}", // Size: 'compact', 'normal', 'invisible' (v2), default value is 'invisible' + "recaptchaSiteKey": "{YOUR_RECAPTCHA_SITE_KEY}", // Site key for requests, default value is '' + "recaptchaSecretkey": "{YOUR_RECAPTCHA_SECRET_KEY}", // Secret key for requests, default value is '' + "recaptchaVersion": "{YOUR_RECAPTCHA_VERSION}", // Version 2 or 3, default value is 3 + "recaptchaMinScore": "{YOUR_RECAPTCHA_MIN_SCORE}" // The min score used for v3, default value is 0.5 + ... +} +``` + +### Sample configuration + +```json5 +{ + ... + "recaptchaEnabled": true, + "recaptchaHideBadge": false, + "recaptchaSize": "invisible", + "recaptchaSiteKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "recaptchaSecretkey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "recaptchaVersion": 3, + "recaptchaMinScore": 0.5 ... } ``` diff --git a/packages/api-client/src/api/createCustomer/index.ts b/packages/api-client/src/api/createCustomer/index.ts index 49c680629..a3f45e4a4 100644 --- a/packages/api-client/src/api/createCustomer/index.ts +++ b/packages/api-client/src/api/createCustomer/index.ts @@ -20,7 +20,7 @@ export default async ( recaptchaToken, ...variables } = input; - if (context.config.recaptcha.secretkey) { + if (context.config.recaptcha.isEnabled) { /** * recaptcha token verification */ @@ -28,7 +28,7 @@ export default async ( if (!response.success) { return { - errors: [new GraphQLError('Invalid token')], + errors: [new GraphQLError('Error during reCaptcha verification. Please try again.')], data: null, }; } diff --git a/packages/api-client/src/api/createProductReview/index.ts b/packages/api-client/src/api/createProductReview/index.ts index 6d8258eff..f227a98d4 100644 --- a/packages/api-client/src/api/createProductReview/index.ts +++ b/packages/api-client/src/api/createProductReview/index.ts @@ -15,7 +15,7 @@ export default async ( recaptchaToken, ...variables } = input; - if (context.config.recaptcha.secretkey) { + if (context.config.recaptcha.isEnabled) { /** * recaptcha token verification */ @@ -23,7 +23,7 @@ export default async ( if (!response.success) { return { - errors: [new GraphQLError('Invalid token')], + errors: [new GraphQLError('Error during reCaptcha verification. Please try again.')], data: null, }; } diff --git a/packages/api-client/src/api/generateCustomerToken/index.ts b/packages/api-client/src/api/generateCustomerToken/index.ts index 8c694d2c7..917f33267 100644 --- a/packages/api-client/src/api/generateCustomerToken/index.ts +++ b/packages/api-client/src/api/generateCustomerToken/index.ts @@ -19,7 +19,7 @@ export default async ( customQuery: CustomQuery = { generateCustomerToken: 'generateCustomerToken' }, ): Promise> => { try { - if (context.config.recaptcha.secretkey) { + if (context.config.recaptcha.isEnabled) { /** * recaptcha token verification */ @@ -27,7 +27,7 @@ export default async ( if (!response.success) { return { - errors: [new GraphQLError('Invalid token')], + errors: [new GraphQLError('Error during reCaptcha verification. Please try again.')], data: null, }; } diff --git a/packages/api-client/src/api/requestPasswordResetEmail/index.ts b/packages/api-client/src/api/requestPasswordResetEmail/index.ts index dd9b588df..4550c46a3 100644 --- a/packages/api-client/src/api/requestPasswordResetEmail/index.ts +++ b/packages/api-client/src/api/requestPasswordResetEmail/index.ts @@ -18,7 +18,7 @@ export default async ( recaptchaToken, ...variables } = input; - if (context.config.recaptcha.secretkey) { + if (context.config.recaptcha.isEnabled) { /** * recaptcha token verification */ @@ -26,7 +26,7 @@ export default async ( if (!response.success) { return { - errors: [new GraphQLError('Invalid token')], + errors: [new GraphQLError('Error during reCaptcha verification. Please try again.')], data: null, }; } diff --git a/packages/api-client/src/api/resetPassword/index.ts b/packages/api-client/src/api/resetPassword/index.ts index 5cb585b80..acc54945f 100644 --- a/packages/api-client/src/api/resetPassword/index.ts +++ b/packages/api-client/src/api/resetPassword/index.ts @@ -19,7 +19,7 @@ export default async ( recaptchaToken, ...variables } = input; - if (context.config.recaptcha.secretkey) { + if (context.config.recaptcha.isEnabled) { /** * recaptcha token verification */ @@ -27,7 +27,7 @@ export default async ( if (!response.success) { return { - errors: [new GraphQLError('Invalid token')], + errors: [new GraphQLError('Error during reCaptcha verification. Please try again.')], data: null, }; } diff --git a/packages/api-client/src/types/setup.ts b/packages/api-client/src/types/setup.ts index c24d51c64..ac480ecc0 100644 --- a/packages/api-client/src/types/setup.ts +++ b/packages/api-client/src/types/setup.ts @@ -72,6 +72,7 @@ export interface ClientConfig { } export interface RecaptchaConfig { + isEnabled: boolean, sitekey: string, secretkey: string, version: number, diff --git a/packages/theme/components/LoginModal.vue b/packages/theme/components/LoginModal.vue index 3a2aa8645..4ae28106f 100644 --- a/packages/theme/components/LoginModal.vue +++ b/packages/theme/components/LoginModal.vue @@ -339,8 +339,9 @@ export default defineComponent({ const isForgotten = ref(false); const isThankYouAfterForgotten = ref(false); const userEmail = ref(''); - const { $recaptcha } = useContext(); - const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && !!$recaptcha.siteKey); + const { $recaptcha, $config } = useContext(); + const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && $config.isRecaptcha); + const { register, login, diff --git a/packages/theme/components/ProductAddReviewForm.vue b/packages/theme/components/ProductAddReviewForm.vue index eb88aae63..46d637670 100644 --- a/packages/theme/components/ProductAddReviewForm.vue +++ b/packages/theme/components/ProductAddReviewForm.vue @@ -167,8 +167,8 @@ export default defineComponent({ setup(_, { emit }) { const route = useRoute(); const { params: { id } } = route.value; - const { $recaptcha } = useContext(); - const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && !!$recaptcha.siteKey); + const { $recaptcha, $config } = useContext(); + const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && $config.isRecaptcha); const { loading, loadReviewMetadata, diff --git a/packages/theme/config.js b/packages/theme/config.js index 012b37d30..905daed48 100644 --- a/packages/theme/config.js +++ b/packages/theme/config.js @@ -74,6 +74,12 @@ const config = convict({ env: 'IMAGE_PROVIDER_BASE_URL', }, // region recaptcha + recaptchaEnabled: { + doc: 'reCaptcha Enabled', + format: Boolean, + default: process.env.RECAPTCHA_ENABLED || false, + env: 'RECAPTCHA_ENABLED', + }, recaptchaHideBadge: { doc: 'reCaptcha Hide Badge', format: Boolean, diff --git a/packages/theme/middleware.config.js b/packages/theme/middleware.config.js index 878ee1a65..e7e79cd5b 100755 --- a/packages/theme/middleware.config.js +++ b/packages/theme/middleware.config.js @@ -28,6 +28,7 @@ module.exports = { magentoBaseUrl: config.get('magentoBaseUrl'), imageProvider: config.get('imageProvider'), recaptcha: { + isEnabled: config.get('recaptchaEnabled'), sitekey: config.get('recaptchaSiteKey'), secretkey: config.get('recaptchaSecretkey'), version: config.get('recaptchaVersion'), diff --git a/packages/theme/nuxt.config.js b/packages/theme/nuxt.config.js index ce6009c9e..1e9618d75 100755 --- a/packages/theme/nuxt.config.js +++ b/packages/theme/nuxt.config.js @@ -121,6 +121,9 @@ export default { version: config.get('recaptchaVersion'), // Version 2 or 3 size: config.get('recaptchaSize'), // Size: 'compact', 'normal', 'invisible' (v2) }, + publicRuntimeConfig: { + isRecaptcha: config.get('recaptchaEnabled'), + }, i18n: { country: 'US', strategy: 'prefix', diff --git a/packages/theme/pages/Checkout/UserAccount.vue b/packages/theme/pages/Checkout/UserAccount.vue index 815707596..3652d9174 100644 --- a/packages/theme/pages/Checkout/UserAccount.vue +++ b/packages/theme/pages/Checkout/UserAccount.vue @@ -182,8 +182,8 @@ export default defineComponent({ }, setup() { const router = useRouter(); - const { app, $recaptcha } = useContext(); - const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && !!$recaptcha.siteKey); + const { app, $recaptcha, $config } = useContext(); + const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && $config.isRecaptcha); const { attachToCart, diff --git a/packages/theme/pages/ResetPassword.vue b/packages/theme/pages/ResetPassword.vue index ef502da18..c37e30fe1 100644 --- a/packages/theme/pages/ResetPassword.vue +++ b/packages/theme/pages/ResetPassword.vue @@ -149,8 +149,8 @@ export default defineComponent({ const isPasswordChanged = computed(() => forgotPasswordGetters.isPasswordChanged(result.value)); const { token } = context.root.$route.query; - const { $recaptcha } = useContext(); - const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && !!$recaptcha.siteKey); + const { $recaptcha, $config } = useContext(); + const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && $config.isRecaptcha); const setNewPassword = async () => { passwordMatchError.value = false; From a52f8d36dacf185c387259931bd87df5fec3e556 Mon Sep 17 00:00:00 2001 From: Abdellatif El Mizeb Date: Fri, 21 Jan 2022 12:29:10 +0100 Subject: [PATCH 14/15] feat(recaptcha): refactoring --- packages/theme/components/LoginModal.vue | 22 +++++++++---------- .../theme/components/ProductAddReviewForm.vue | 12 +++++----- packages/theme/pages/Checkout/UserAccount.vue | 14 ++++++------ packages/theme/pages/ResetPassword.vue | 12 +++++----- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/theme/components/LoginModal.vue b/packages/theme/components/LoginModal.vue index 4ae28106f..48908d8a0 100644 --- a/packages/theme/components/LoginModal.vue +++ b/packages/theme/components/LoginModal.vue @@ -56,7 +56,7 @@ class="form__element" /> - +
{{ error.login }}
@@ -119,7 +119,7 @@ class="form__element" /> - +
{{ $t('It was not possible to request a new password, please check the entered email address.') }}
@@ -246,7 +246,7 @@ class="form__element" /> - +
{{ error.register }}
@@ -340,7 +340,7 @@ export default defineComponent({ const isThankYouAfterForgotten = ref(false); const userEmail = ref(''); const { $recaptcha, $config } = useContext(); - const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && $config.isRecaptcha); + const isRecaptchaEnabled = ref(typeof $recaptcha !== 'undefined' && $config.isRecaptcha); const { register, @@ -396,11 +396,11 @@ export default defineComponent({ const handleForm = (fn) => async () => { resetErrorValues(); - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { $recaptcha.init(); } - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { const recaptchaToken = await $recaptcha.getResponse(); form.value.recaptchaInstance = $recaptcha; @@ -428,7 +428,7 @@ export default defineComponent({ } toggleLoginModal(); - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { // reset recaptcha $recaptcha.reset(); } @@ -441,11 +441,11 @@ export default defineComponent({ const handleForgotten = async () => { userEmail.value = form.value.username; - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { $recaptcha.init(); } - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { const recaptchaToken = await $recaptcha.getResponse(); await request({ email: userEmail.value, recaptchaToken }); @@ -458,7 +458,7 @@ export default defineComponent({ isForgotten.value = false; } - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { // reset recaptcha $recaptcha.reset(); } @@ -486,7 +486,7 @@ export default defineComponent({ setIsLoginValue, userEmail, userError, - isRecaptcha, + isRecaptchaEnabled, }; }, }); diff --git a/packages/theme/components/ProductAddReviewForm.vue b/packages/theme/components/ProductAddReviewForm.vue index 46d637670..39d2aa3bb 100644 --- a/packages/theme/components/ProductAddReviewForm.vue +++ b/packages/theme/components/ProductAddReviewForm.vue @@ -102,7 +102,7 @@ />
- + Add review @@ -168,7 +168,7 @@ export default defineComponent({ const route = useRoute(); const { params: { id } } = route.value; const { $recaptcha, $config } = useContext(); - const isRecaptcha = ref(typeof $recaptcha !== 'undefined' && $config.isRecaptcha); + const isRecaptchaEnabled = ref(typeof $recaptcha !== 'undefined' && $config.isRecaptcha); const { loading, loadReviewMetadata, @@ -212,11 +212,11 @@ export default defineComponent({ || formSubmitValue.value.text )) return; try { - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { $recaptcha.init(); } - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { const recaptchaToken = await $recaptcha.getResponse(); formSubmitValue.value.recaptchaToken = recaptchaToken; } @@ -227,7 +227,7 @@ export default defineComponent({ reset(); - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { $recaptcha.reset(); } } catch { @@ -248,7 +248,7 @@ export default defineComponent({ ratingMetadata, reviewSent, submitForm, - isRecaptcha, + isRecaptchaEnabled, }; }, }); diff --git a/packages/theme/pages/Checkout/UserAccount.vue b/packages/theme/pages/Checkout/UserAccount.vue index 3652d9174..383e41845 100644 --- a/packages/theme/pages/Checkout/UserAccount.vue +++ b/packages/theme/pages/Checkout/UserAccount.vue @@ -110,7 +110,7 @@ class="form__element" :disabled="createUserAccount" /> - +
async () => { - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { $recaptcha.init(); } if (!isAuthenticated.value) { - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { const recaptchaToken = await $recaptcha.getResponse(); form.value.recaptchaToken = recaptchaToken; form.value.recaptchaInstance = $recaptcha; @@ -239,7 +239,7 @@ export default defineComponent({ } if (loginUserAccount.value) { - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { const recaptchaToken = await $recaptcha.getResponse(); await login({ @@ -275,7 +275,7 @@ export default defineComponent({ }); } - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { // reset recaptcha $recaptcha.reset(); } @@ -311,7 +311,7 @@ export default defineComponent({ loading, loginUserAccount, user, - isRecaptcha, + isRecaptchaEnabled, }; }, }); diff --git a/packages/theme/pages/ResetPassword.vue b/packages/theme/pages/ResetPassword.vue index c37e30fe1..911d64815 100644 --- a/packages/theme/pages/ResetPassword.vue +++ b/packages/theme/pages/ResetPassword.vue @@ -61,7 +61,7 @@
{{ passwordMatchError || forgotPasswordError.setNew.message }}
- + { passwordMatchError.value = false; @@ -159,11 +159,11 @@ export default defineComponent({ return; } - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { $recaptcha.init(); } - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { const recaptchaToken = await $recaptcha.getResponse(); await setNew({ @@ -180,7 +180,7 @@ export default defineComponent({ }); } - if (isRecaptcha.value) { + if (isRecaptchaEnabled.value) { $recaptcha.reset(); } }; @@ -194,7 +194,7 @@ export default defineComponent({ passwordMatchError, token, result, - isRecaptcha, + isRecaptchaEnabled, }; }, }); From 2cb4724678bf254df10fe34574bb0e0870462cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Le=20Menach?= Date: Tue, 25 Jan 2022 11:21:24 +0100 Subject: [PATCH 15/15] feat(recaptcha): add tests --- .../theme/components/ProductAddReviewForm.vue | 4 +- .../components/__tests__/LoginModal.spec.js | 211 ++++++++++++++++++ .../__tests__/ProductAddReviewForm.spec.js | 133 +++++++++++ packages/theme/package.json | 3 +- packages/theme/pages/Checkout/UserAccount.vue | 28 +-- .../Checkout/__tests__/UserAccount.spec.js | 157 ++++++++++++- .../pages/__tests__/ResetPassword.spec.js | 71 ++++++ packages/theme/test-utils.js | 13 +- packages/theme/test-utils/mocks/index.js | 4 +- .../test-utils/mocks/useForgotPassword.js | 11 + packages/theme/test-utils/mocks/useReview.js | 26 +++ packages/theme/test-utils/mocks/useUser.js | 18 +- 12 files changed, 637 insertions(+), 42 deletions(-) create mode 100644 packages/theme/components/__tests__/LoginModal.spec.js create mode 100644 packages/theme/components/__tests__/ProductAddReviewForm.spec.js create mode 100644 packages/theme/pages/__tests__/ResetPassword.spec.js create mode 100644 packages/theme/test-utils/mocks/useForgotPassword.js create mode 100644 packages/theme/test-utils/mocks/useReview.js diff --git a/packages/theme/components/ProductAddReviewForm.vue b/packages/theme/components/ProductAddReviewForm.vue index 39d2aa3bb..fb535b759 100644 --- a/packages/theme/components/ProductAddReviewForm.vue +++ b/packages/theme/components/ProductAddReviewForm.vue @@ -97,6 +97,7 @@ :cols="60" :rows="10" wrap="soft" + required :valid="!errors[0]" :error-message="errors[0]" /> @@ -192,13 +193,12 @@ export default defineComponent({ id: key, value_id: `${form.value.ratings[key]}`, })); - const recaptchaToken = ''; return { ...form.value, nickname, ratings, - recaptchaToken, + recaptchaToken: '', }; }); diff --git a/packages/theme/components/__tests__/LoginModal.spec.js b/packages/theme/components/__tests__/LoginModal.spec.js new file mode 100644 index 000000000..e25bdad36 --- /dev/null +++ b/packages/theme/components/__tests__/LoginModal.spec.js @@ -0,0 +1,211 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { ref } from '@nuxtjs/composition-api'; +import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/vue'; +import { useUser, useForgotPassword } from '@vue-storefront/magento'; +import { render, useUserMock, useForgotPasswordMock } from '~/test-utils'; +import useUiState from '~/composables/useUiState.ts'; + +import LoginModal from '../LoginModal'; + +jest.mock('~/composables/useUiState.ts', () => jest.fn()); +jest.mock('@vue-storefront/magento', () => ({ + useUser: jest.fn(), + useForgotPassword: jest.fn(), +})); + +describe('', () => { + it('User can log in', async () => { + useUiState.mockReturnValue({ + isLoginModalOpen: ref(true), + toggleLoginModal: jest.fn(), + }); + const loginMock = jest.fn(); + useForgotPassword.mockReturnValue(useForgotPasswordMock()); + useUser.mockReturnValue(useUserMock({ + login: loginMock, + })); + + const values = { + email: 'james@bond.io', + password: 'J@mesBond007!', + token: 'recaptcha token', + }; + + const $recaptchaInstance = { + init: jest.fn(), + reset: jest.fn(), + getResponse: jest.fn().mockResolvedValue(Promise.resolve(values.token)), + }; + const { getByRole, findByLabelText, queryByTestId } = render(LoginModal, { + mocks: { + $nuxt: { + context: { + $config: { isRecaptcha: true }, + $recaptcha: $recaptchaInstance, + }, + }, + }, + }); + + const recaptchaComponent = queryByTestId('recaptcha'); + expect(recaptchaComponent).toBeTruthy(); + + const emailInput = getByRole('textbox', { name: /your email/i }); + const passwordInput = await findByLabelText('Password'); + + userEvent.type(emailInput, values.email); + userEvent.type(passwordInput, values.password); + + const submitButton = getByRole('button', { name: /login/i }); + userEvent.click(submitButton); + + await waitFor(() => { + expect(loginMock).toHaveBeenCalledTimes(1); + expect(loginMock).toHaveBeenCalledWith({ + user: { + username: values.email, + password: values.password, + is_subscribed: false, + recaptchaInstance: $recaptchaInstance, + recaptchaToken: values.token, + }, + }); + }); + }); + + it('User can register', async () => { + const registerMock = jest.fn(); + useForgotPassword.mockReturnValue(useForgotPasswordMock()); + useUser.mockReturnValue(useUserMock({ + register: registerMock, + })); + useUiState.mockReturnValue({ + isLoginModalOpen: ref(true), + toggleLoginModal: jest.fn(), + }); + + const values = { + email: 'james@bond.io', + firstName: 'James', + lastName: 'Bond', + password: 'J@mesBond007!', + token: 'recaptcha token', + }; + + const $recaptchaInstance = { + init: jest.fn(), + reset: jest.fn(), + getResponse: jest.fn().mockResolvedValue(Promise.resolve(values.token)), + }; + const { + getByRole, + findByRole, + findByLabelText, + queryByTestId, + } = render(LoginModal, { + mocks: { + $nuxt: { + context: { + $config: { isRecaptcha: true }, + $recaptcha: $recaptchaInstance, + }, + }, + }, + }); + + const switchToRegisterButton = getByRole('button', { name: /register today/i }); + userEvent.click(switchToRegisterButton); + + await waitFor(() => findByRole('button', { name: /create an account/i })); + + const recaptchaComponent = queryByTestId('recaptcha'); + expect(recaptchaComponent).toBeTruthy(); + + const emailInput = getByRole('textbox', { name: /your email/i }); + const firstNameInput = getByRole('textbox', { name: /first name/i }); + const lastNameInput = getByRole('textbox', { name: /last name/i }); + const passwordInput = await findByLabelText('Password'); + const newsletterCheckbox = getByRole('checkbox', { name: /sign up for newsletter/i }); + const createAccountCheckbox = getByRole('checkbox', { name: /i want to create an account/i }); + + userEvent.type(emailInput, values.email); + userEvent.type(firstNameInput, values.firstName); + userEvent.type(lastNameInput, values.lastName); + userEvent.type(passwordInput, values.password); + userEvent.click(newsletterCheckbox); + userEvent.click(createAccountCheckbox); + + const submitButton = getByRole('button', { name: /create an account/i }); + userEvent.click(submitButton); + + await waitFor(() => { + expect(registerMock).toHaveBeenCalledTimes(1); + expect(registerMock).toHaveBeenCalledWith({ + user: { + email: values.email, + firstName: values.firstName, + is_subscribed: true, + lastName: values.lastName, + password: values.password, + recaptchaInstance: $recaptchaInstance, + recaptchaToken: values.token, + }, + }); + }); + }); + + it('User can reset his password', async () => { + const requestPasswordMock = jest.fn(); + useForgotPassword.mockReturnValue(useForgotPasswordMock({ + request: requestPasswordMock, + })); + useUser.mockReturnValue(useUserMock()); + useUiState.mockReturnValue({ + isLoginModalOpen: ref(true), + toggleLoginModal: jest.fn(), + }); + + const values = { + email: 'james@bond.io', + token: 'recaptcha token', + }; + + const { getByRole, findByRole, queryByTestId } = render(LoginModal, { + mocks: { + $nuxt: { + context: { + $config: { isRecaptcha: true }, + $recaptcha: { + init: jest.fn(), + reset: jest.fn(), + getResponse: jest.fn().mockResolvedValue(Promise.resolve(values.token)), + }, + }, + }, + }, + }); + + const forgottenPasswordButton = getByRole('button', { name: /forgotten password/i }); + userEvent.click(forgottenPasswordButton); + + await waitFor(() => findByRole('button', { name: /reset password/i })); + + const recaptchaComponent = queryByTestId('recaptcha'); + expect(recaptchaComponent).toBeTruthy(); + + const emailInput = getByRole('textbox', { name: /email/i }); + userEvent.type(emailInput, values.email); + + const submitButton = getByRole('button', { name: /reset password/i }); + userEvent.click(submitButton); + + await waitFor(() => { + expect(requestPasswordMock).toHaveBeenCalledTimes(1); + expect(requestPasswordMock).toHaveBeenCalledWith({ + email: values.email, + recaptchaToken: values.token, + }); + }); + }); +}); diff --git a/packages/theme/components/__tests__/ProductAddReviewForm.spec.js b/packages/theme/components/__tests__/ProductAddReviewForm.spec.js new file mode 100644 index 000000000..36891fd18 --- /dev/null +++ b/packages/theme/components/__tests__/ProductAddReviewForm.spec.js @@ -0,0 +1,133 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { waitFor } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import { useRoute } from '@nuxtjs/composition-api'; +import { useUser, useReview } from '@vue-storefront/magento'; +import { + render, + useUserMock, + useReviewMock, +} from '~/test-utils'; + +import ProductAddReviewForm from '../ProductAddReviewForm'; + +jest.mock('@vue-storefront/magento', () => { + const originalModule = jest.requireActual('@vue-storefront/magento'); + return { + ...originalModule, + useUser: jest.fn(), + useReview: jest.fn(), + }; +}); + +jest.mock('@nuxtjs/composition-api', () => { + // Require the original module to not be mocked... + const originalModule = jest.requireActual('@nuxtjs/composition-api'); + + return { + ...originalModule, + useRoute: jest.fn(), + }; +}); + +describe('', () => { + it('Form fields are rendered and validated', async () => { + useUser.mockReturnValue(useUserMock()); + useReview.mockReturnValue(useReviewMock()); + useRoute.mockReturnValue({ value: { params: { id: '' } } }); + + const { getByRole, findAllByText, queryByTestId } = render(ProductAddReviewForm); + + // Nickname, title and review fields should be rendered and required + + const recaptchaComponent = queryByTestId('recaptcha'); + expect(recaptchaComponent).toBeNull(); + + const nickname = getByRole('textbox', { name: /name/i }); + expect(nickname).toBeRequired(); + + const summary = getByRole('textbox', { name: /title/i }); + expect(summary).toBeRequired(); + + const text = getByRole('textbox', { name: /review/i }); + expect(text).toBeRequired(); + + const submitButton = getByRole('button', { name: /add review/i }); + userEvent.click(submitButton); + + // should display form errors when field are not filled + const errors = await findAllByText('This field is required'); + expect(errors).toHaveLength(3); + }); + + it('User can submit a review', async () => { + const values = { + nickname: 'nickname value', + rating: '2', + sku: 'sku value', + summary: 'summary value', + text: 'text value', + token: 'token value', + }; + + useRoute.mockReturnValue({ value: { params: { id: values.sku } } }); + useUser.mockReturnValue(useUserMock()); + useReview.mockReturnValue(useReviewMock({ + metadata: { + value: [{ + id: 'rating', + name: 'Product rating', + values: [ + { value_id: '1', value: 'Rating 1' }, + { value_id: '2', value: 'Rating 2' }, + { value_id: '3', value: 'Rating 3' }, + ], + }], + }, + })); + + const { getByRole, emitted, queryByTestId } = render(ProductAddReviewForm, { + mocks: { + $nuxt: { + context: { + $config: { isRecaptcha: true }, + $recaptcha: { + init: jest.fn(), + getResponse: jest.fn().mockResolvedValue(Promise.resolve(values.token)), + }, + }, + }, + }, + }); + + const recaptchaComponent = queryByTestId('recaptcha'); + expect(recaptchaComponent).toBeTruthy(); + + const nickname = getByRole('textbox', { name: /name/i }); + const summary = getByRole('textbox', { name: /title/i }); + const text = getByRole('textbox', { name: /review/i }); + const rating = getByRole('combobox', { name: /product rating/i }); + const submitButton = getByRole('button', { name: /add review/i }); + + // fill the form + userEvent.type(nickname, values.nickname); + userEvent.type(summary, values.summary); + userEvent.selectOptions(rating, values.rating); + userEvent.type(text, values.text); + + // Submit the form + userEvent.click(submitButton); + + await waitFor(() => { + expect(emitted()).toHaveProperty('add-review'); + expect(emitted()['add-review'][0][0]).toEqual({ + nickname: values.nickname, + ratings: [{ id: 'rating', value_id: values.rating }], + sku: values.sku, + summary: values.summary, + text: values.text, + recaptchaToken: values.token, + }); + }); + }); +}); diff --git a/packages/theme/package.json b/packages/theme/package.json index 772d0adb9..0817db03d 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -67,6 +67,7 @@ "cypress": "^9.2.1", "cypress-pipe": "^2.0.0", "cypress-tags": "^0.3.0", + "deepmerge": "^4.2.2", "dotenv": "^12.0.1", "ejs": "^3.1.6", "jest": "^27.4.7", @@ -99,4 +100,4 @@ "engines": { "node": ">=16.x" } -} +} \ No newline at end of file diff --git a/packages/theme/pages/Checkout/UserAccount.vue b/packages/theme/pages/Checkout/UserAccount.vue index 383e41845..00810b3ea 100644 --- a/packages/theme/pages/Checkout/UserAccount.vue +++ b/packages/theme/pages/Checkout/UserAccount.vue @@ -225,7 +225,7 @@ export default defineComponent({ } if (!isAuthenticated.value) { - if (isRecaptchaEnabled.value) { + if (isRecaptchaEnabled.value && createUserAccount.value) { const recaptchaToken = await $recaptcha.getResponse(); form.value.recaptchaToken = recaptchaToken; form.value.recaptchaInstance = $recaptcha; @@ -239,24 +239,18 @@ export default defineComponent({ } if (loginUserAccount.value) { + const recaptchaParams = {}; if (isRecaptchaEnabled.value) { - const recaptchaToken = await $recaptcha.getResponse(); - - await login({ - user: { - username: form.value.email, - password: form.value.password, - recaptchaToken, - }, - }); - } else { - await login({ - user: { - username: form.value.email, - password: form.value.password, - }, - }); + recaptchaParams.recaptchaToken = await $recaptcha.getResponse(); } + + await login({ + user: { + username: form.value.email, + password: form.value.password, + ...recaptchaParams, + }, + }); } if (!hasError.value) { diff --git a/packages/theme/pages/Checkout/__tests__/UserAccount.spec.js b/packages/theme/pages/Checkout/__tests__/UserAccount.spec.js index 014f84ba6..1bed0c4c8 100644 --- a/packages/theme/pages/Checkout/__tests__/UserAccount.spec.js +++ b/packages/theme/pages/Checkout/__tests__/UserAccount.spec.js @@ -7,15 +7,18 @@ import { render, useUserMock, useGuestUserMock } from '~/test-utils'; import UserAccount from '../UserAccount'; +jest.mock('~/helpers/asyncLocalStorage', () => ({ + getItem: jest.fn(), + mergeItem: jest.fn(), +})); + jest.mock('@vue-storefront/magento', () => ({ useGuestUser: jest.fn(), useUser: jest.fn(), })); jest.mock('@nuxtjs/composition-api', () => { - // Require the original module to not be mocked... const originalModule = jest.requireActual('@nuxtjs/composition-api'); - return { ...originalModule, useRouter: jest.fn(), @@ -27,10 +30,13 @@ describe('', () => { useUser.mockReturnValue(useUserMock()); useGuestUser.mockReturnValue(useGuestUserMock()); - const { getByRole, findAllByText } = render(UserAccount); + const { getByRole, findAllByText, queryByTestId } = render(UserAccount); // First name, last name and email fields should be rendered and required + const recaptchaComponent = queryByTestId('recaptcha'); + expect(recaptchaComponent).toBeNull(); + const firstNameInput = getByRole('textbox', { name: /first name/i }); expect(firstNameInput).toBeRequired(); @@ -101,11 +107,150 @@ describe('', () => { expect(routerPushMock).toHaveBeenCalledWith('/checkout/shipping'); }); - test.todo('User cannot move to the next step if data is loading'); + it('User can log-in to the store', async () => { + const attachToCartMock = jest.fn(); + const routerPushMock = jest.fn(); + const loginMock = jest.fn(); + + useUser.mockReturnValue(useUserMock({ + login: loginMock, + })); + useGuestUser.mockReturnValue(useGuestUserMock({ + attachToCart: attachToCartMock, + })); + useRouter.mockReturnValue({ + push: routerPushMock, + }); + + const values = { + password: 'J@mesBond007!', + email: 'james@bond.io', + token: 'token value', + }; + + const { findByLabelText, getByRole, queryByTestId } = render(UserAccount, { + mocks: { + $nuxt: { + context: { + $config: { isRecaptcha: true }, + $recaptcha: { + init: jest.fn(), + reset: jest.fn(), + getResponse: jest.fn().mockResolvedValue(Promise.resolve(values.token)), + }, + }, + }, + }, + }); + + const recaptchaComponent = queryByTestId('recaptcha'); + expect(recaptchaComponent).toBeTruthy(); + + const loginCheckbox = getByRole('checkbox', { name: /login on the store/i }); + userEvent.click(loginCheckbox); + + const passwordInput = await findByLabelText('Password'); + const emailInput = getByRole('textbox', { name: /e-mail/i }); + const continueButton = getByRole('button', { name: /continue to shipping/i }); + + userEvent.type(passwordInput, values.password); + userEvent.type(emailInput, values.email); + + // click the continue button + userEvent.click(continueButton); + + await waitFor(() => { + expect(attachToCartMock).toHaveBeenCalledTimes(1); + expect(attachToCartMock).toHaveBeenCalledWith({ + email: 'james@bond.io', + }); - test.todo('User can log-in to the store'); + expect(loginMock).toHaveBeenCalledTimes(1); + expect(loginMock).toHaveBeenCalledWith({ + user: { + username: values.email, + password: values.password, + recaptchaToken: values.token, + }, + }); + }); + expect(routerPushMock).toHaveBeenCalledTimes(1); + expect(routerPushMock).toHaveBeenCalledWith('/checkout/shipping'); + }); - test.todo('User can create an account'); + it('User can create an account', async () => { + const routerPushMock = jest.fn(); + const registerMock = jest.fn(); + + useUser.mockReturnValue(useUserMock({ + register: registerMock, + })); + useRouter.mockReturnValue({ + push: routerPushMock, + }); + + const values = { + password: 'J@mesBond007!', + firstName: 'James', + lastName: 'Bond', + email: 'james@bond.io', + token: 'token value', + }; + + const recaptchaInstance = { + init: jest.fn(), + reset: jest.fn(), + getResponse: jest.fn().mockResolvedValue(Promise.resolve(values.token)), + }; + const { findByLabelText, findByRole, getByRole } = render(UserAccount, { + mocks: { + $nuxt: { + context: { + $config: { isRecaptcha: true }, + $recaptcha: recaptchaInstance, + }, + }, + }, + }); + + const loginCheckbox = getByRole('checkbox', { name: /create an account on the store/i }); + userEvent.click(loginCheckbox); + + const passwordInput = await findByLabelText('Password'); + const newsletterCheckbox = await findByRole('checkbox', { name: /sign up for newsletter/i }); + const firstNameInput = getByRole('textbox', { name: /first name/i }); + const lastNameInput = getByRole('textbox', { name: /last name/i }); + const emailInput = getByRole('textbox', { name: /e-mail/i }); + const continueButton = getByRole('button', { name: /continue to shipping/i }); + + userEvent.click(newsletterCheckbox); + userEvent.type(passwordInput, values.password); + userEvent.type(emailInput, values.email); + userEvent.type(firstNameInput, values.firstName); + userEvent.type(lastNameInput, values.lastName); + + // click the continue button + userEvent.click(continueButton); + + await waitFor(() => { + expect(registerMock).toHaveBeenCalledTimes(1); + expect(registerMock).toHaveBeenCalledWith({ + user: { + email: values.email, + is_subscribed: true, + firstname: values.firstName, + lastname: values.lastName, + recaptchaInstance, + password: values.password, + recaptchaToken: values.token, + }, + }); + }); + expect(routerPushMock).toHaveBeenCalledTimes(1); + expect(routerPushMock).toHaveBeenCalledWith('/checkout/shipping'); + }); + + test.todo('User cannot move to the next step if data is loading'); test.todo('User can subscribe to the newsletter during account creation'); }); diff --git a/packages/theme/pages/__tests__/ResetPassword.spec.js b/packages/theme/pages/__tests__/ResetPassword.spec.js new file mode 100644 index 000000000..12b02a4f1 --- /dev/null +++ b/packages/theme/pages/__tests__/ResetPassword.spec.js @@ -0,0 +1,71 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/vue'; +import { useForgotPassword } from '@vue-storefront/magento'; +import { render, useForgotPasswordMock } from '~/test-utils'; + +import ResetPassword from '../ResetPassword'; + +jest.mock('@vue-storefront/magento', () => { + const originalModule = jest.requireActual('@vue-storefront/magento'); + return { + ...originalModule, + useForgotPassword: jest.fn(), + }; +}); + +describe('', () => { + it('User can change his password', async () => { + const setNewMock = jest.fn(); + useForgotPassword.mockReturnValue(useForgotPasswordMock({ + setNew: setNewMock, + })); + + const values = { + email: 'james@bond.io', + password: 'J@mesBond007!', + token: 'token value', + resetToken: 'reset token value', + }; + + const { getByRole, findByLabelText, queryByTestId } = render(ResetPassword, { + mocks: { + $route: { query: { token: values.resetToken } }, + $nuxt: { + context: { + $config: { isRecaptcha: true }, + $recaptcha: { + init: jest.fn(), + reset: jest.fn(), + getResponse: jest.fn().mockResolvedValue(Promise.resolve(values.token)), + }, + }, + }, + }, + }); + + const recaptchaComponent = queryByTestId('recaptcha'); + expect(recaptchaComponent).toBeTruthy(); + + const emailInput = getByRole('textbox', /your email/i); + const passwordInput = await findByLabelText('Password'); + const repeatPasswordInput = await findByLabelText('Repeat Password'); + + userEvent.type(emailInput, values.email); + userEvent.type(passwordInput, values.password); + userEvent.type(repeatPasswordInput, values.password); + + const continueButton = getByRole('button', { name: /save password/i }); + userEvent.click(continueButton); + + await waitFor(() => { + expect(setNewMock).toHaveBeenCalledTimes(1); + expect(setNewMock).toHaveBeenCalledWith({ + email: values.email, + newPassword: values.password, + recaptchaToken: values.token, + tokenValue: values.resetToken, + }); + }); + }); +}); diff --git a/packages/theme/test-utils.js b/packages/theme/test-utils.js index eb0f16821..49ed92f8b 100644 --- a/packages/theme/test-utils.js +++ b/packages/theme/test-utils.js @@ -1,11 +1,12 @@ import { render } from '@testing-library/vue'; +import deepmerge from 'deepmerge'; const $t = (text) => text; const $n = (text) => text; const $fc = (text) => text; const localePath = (path) => path; // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -const customRender = (component, options = {}, callback = null) => render(component, { +const customRender = (component, options = {}, callback = null) => render(component, deepmerge({ mocks: { $t, $n, @@ -21,16 +22,20 @@ const customRender = (component, options = {}, callback = null) => render(compon }, }, }, - ...options?.mocks, }, stubs: { NuxtImg: { template: 'image', }, + recaptcha: { + template: '
', + }, + i18n: { + template: '
', + }, }, - ...options, // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -}, callback); +}, options), callback); export * from '@testing-library/vue'; export * from '~/test-utils/mocks'; diff --git a/packages/theme/test-utils/mocks/index.js b/packages/theme/test-utils/mocks/index.js index 257d86936..784dbf569 100644 --- a/packages/theme/test-utils/mocks/index.js +++ b/packages/theme/test-utils/mocks/index.js @@ -1,9 +1,11 @@ export * from './useBilling'; export * from './useCart'; export * from './useCountrySearch'; +export * from './useForgotPassword'; export * from './useGuestUser'; export * from './useShipping'; +export * from './useUiState'; export * from './useUser'; export * from './useUserBilling'; -export * from './useUiState'; +export * from './useReview'; export * from './cartGetters'; diff --git a/packages/theme/test-utils/mocks/useForgotPassword.js b/packages/theme/test-utils/mocks/useForgotPassword.js new file mode 100644 index 000000000..17b56ec57 --- /dev/null +++ b/packages/theme/test-utils/mocks/useForgotPassword.js @@ -0,0 +1,11 @@ +import { ref } from '@nuxtjs/composition-api'; + +export const useForgotPasswordMock = (passwordData = {}) => ({ + result: ref({}), + setNew: jest.fn(), + error: ref({}), + loading: ref(false), + ...passwordData, +}); + +export default useForgotPasswordMock; diff --git a/packages/theme/test-utils/mocks/useReview.js b/packages/theme/test-utils/mocks/useReview.js new file mode 100644 index 000000000..4c74a01ac --- /dev/null +++ b/packages/theme/test-utils/mocks/useReview.js @@ -0,0 +1,26 @@ +export const useReviewMock = (reviewData = {}) => ({ + loading: { + value: false, + }, + loadReviewMetadata: jest.fn(), + metadata: { + value: [ + { + id: 'METADATA_ID', + name: 'METADATA_NAME', + values: [ + { + value_id: '1', + value: 'VALUE 1', + }, + ], + }, + ], + }, + error: { + value: {}, + }, + ...reviewData, +}); + +export default useReviewMock; diff --git a/packages/theme/test-utils/mocks/useUser.js b/packages/theme/test-utils/mocks/useUser.js index efb133397..87c9b6c76 100644 --- a/packages/theme/test-utils/mocks/useUser.js +++ b/packages/theme/test-utils/mocks/useUser.js @@ -1,16 +1,12 @@ +import { ref } from '@nuxtjs/composition-api'; + export const useUserMock = (userData = {}) => ({ load: jest.fn(), - loading: { - value: false, - }, - isAuthenticated: { - value: false, - }, - error: { - value: { - register: null, - }, - }, + loading: ref(false), + isAuthenticated: ref(false), + error: ref({ + register: null, + }), ...userData, });