From 008f1f09ecab13b0233b9207647f6f5a384895d3 Mon Sep 17 00:00:00 2001 From: Roy Schut Date: Fri, 1 Nov 2024 11:57:28 +0100 Subject: [PATCH] feat(auth): add optional captcha validation to registration --- .../src/controllers/AccountController.ts | 4 +-- .../cleeng/CleengAccountService.ts | 3 ++- packages/common/types/account.ts | 3 +++ packages/ui-react/package.json | 2 ++ .../RecaptchaField/RecaptchaField.module.scss | 6 +++++ .../RecaptchaField/RecaptchaField.tsx | 18 +++++++++++++ .../RegistrationForm/RegistrationForm.tsx | 7 +++++ .../AccountModal/forms/Registration.tsx | 14 ++++++++-- yarn.lock | 27 +++++++++++++++++-- 9 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 packages/ui-react/src/components/RecaptchaField/RecaptchaField.module.scss create mode 100644 packages/ui-react/src/components/RecaptchaField/RecaptchaField.tsx diff --git a/packages/common/src/controllers/AccountController.ts b/packages/common/src/controllers/AccountController.ts index bbb9e4953..9a79dcf2e 100644 --- a/packages/common/src/controllers/AccountController.ts +++ b/packages/common/src/controllers/AccountController.ts @@ -197,9 +197,9 @@ export default class AccountController { await this.refreshEntitlements?.(); }; - register = async (email: string, password: string, referrer: string, consentsValues: CustomerConsent[]) => { + register = async (email: string, password: string, referrer: string, consentsValues: CustomerConsent[], captchaValue?: string) => { try { - const response = await this.accountService.register({ email, password, consents: consentsValues, referrer }); + const response = await this.accountService.register({ email, password, consents: consentsValues, referrer, captchaValue }); if (response) { const { user, customerConsents } = response; diff --git a/packages/common/src/services/integrations/cleeng/CleengAccountService.ts b/packages/common/src/services/integrations/cleeng/CleengAccountService.ts index 4fbf1d2f3..9fe3a8c44 100644 --- a/packages/common/src/services/integrations/cleeng/CleengAccountService.ts +++ b/packages/common/src/services/integrations/cleeng/CleengAccountService.ts @@ -181,7 +181,7 @@ export default class CleengAccountService extends AccountService { }; }; - register: Register = async ({ email, password, consents }) => { + register: Register = async ({ email, password, consents, captchaValue }) => { const localesResponse = await this.getLocales(); this.handleErrors(localesResponse.errors); @@ -194,6 +194,7 @@ export default class CleengAccountService extends AccountService { currency: localesResponse.responseData.currency, publisherId: this.publisherId, customerIP: await this.getCustomerIP(), + captchaValue, }; const { responseData: auth, errors }: ServiceResponse = await this.cleengService.post('/customers', JSON.stringify(payload)); diff --git a/packages/common/types/account.ts b/packages/common/types/account.ts index 603467fce..4c2c47aa2 100644 --- a/packages/common/types/account.ts +++ b/packages/common/types/account.ts @@ -26,6 +26,7 @@ export type LoginArgs = { export type RegistrationArgs = LoginArgs & { consents: CustomerConsent[]; + captchaValue?: string; }; export type AuthResponse = { @@ -49,6 +50,7 @@ export type LoginFormData = { export type RegistrationFormData = { email: string; password: string; + captchaValue?: string | null; }; export type ForgotPasswordFormData = { @@ -88,6 +90,7 @@ export type RegisterPayload = PayloadWithIPOverride & { lastName?: string; externalId?: string; externalData?: string; + captchaValue?: string; }; export type CleengCaptureField = { diff --git a/packages/ui-react/package.json b/packages/ui-react/package.json index 28d1e1c42..8dbfab36e 100644 --- a/packages/ui-react/package.json +++ b/packages/ui-react/package.json @@ -22,6 +22,7 @@ "react": "^18.3.1", "react-app-polyfill": "^3.0.0", "react-dom": "^18.3.1", + "react-google-recaptcha": "^3.1.0", "react-helmet": "^6.1.0", "react-i18next": "^12.3.1", "react-infinite-scroller": "^1.2.6", @@ -38,6 +39,7 @@ "@types/jwplayer": "^8.31.1", "@types/marked": "^4.3.2", "@types/payment": "^2.1.7", + "@types/react-google-recaptcha": "^2.1.9", "@types/react-infinite-scroller": "^1.2.5", "@vitejs/plugin-react": "^4.3.1", "jsdom": "^22.1.0", diff --git a/packages/ui-react/src/components/RecaptchaField/RecaptchaField.module.scss b/packages/ui-react/src/components/RecaptchaField/RecaptchaField.module.scss new file mode 100644 index 000000000..435f0f024 --- /dev/null +++ b/packages/ui-react/src/components/RecaptchaField/RecaptchaField.module.scss @@ -0,0 +1,6 @@ +@use '@jwp/ott-ui-react/src/styles/theme'; + +.captcha { + width: 100%; + margin-bottom: 8px; +} \ No newline at end of file diff --git a/packages/ui-react/src/components/RecaptchaField/RecaptchaField.tsx b/packages/ui-react/src/components/RecaptchaField/RecaptchaField.tsx new file mode 100644 index 000000000..88d076dd7 --- /dev/null +++ b/packages/ui-react/src/components/RecaptchaField/RecaptchaField.tsx @@ -0,0 +1,18 @@ +import ReCaptcha from 'react-google-recaptcha'; +import { forwardRef } from 'react'; + +import styles from './RecaptchaField.module.scss'; + +type Props = { + siteKey: string; +}; + +const RecaptchaField = forwardRef(({ siteKey }, ref) => { + return ( +
+ +
+ ); +}); + +export default RecaptchaField; diff --git a/packages/ui-react/src/components/RegistrationForm/RegistrationForm.tsx b/packages/ui-react/src/components/RegistrationForm/RegistrationForm.tsx index 99cfc4688..e9a9e2290 100644 --- a/packages/ui-react/src/components/RegistrationForm/RegistrationForm.tsx +++ b/packages/ui-react/src/components/RegistrationForm/RegistrationForm.tsx @@ -7,6 +7,7 @@ import type { CustomFormField, RegistrationFormData } from '@jwp/ott-common/type import { testId } from '@jwp/ott-common/src/utils/common'; import type { SocialLoginURLs } from '@jwp/ott-hooks-react/src/useSocialLoginUrls'; import env from '@jwp/ott-common/src/env'; +import type { ReCAPTCHA } from 'react-google-recaptcha'; import TextField from '../form-fields/TextField/TextField'; import Button from '../Button/Button'; @@ -16,6 +17,7 @@ import LoadingOverlay from '../LoadingOverlay/LoadingOverlay'; import Link from '../Link/Link'; import { modalURLFromLocation } from '../../utils/location'; import PasswordField from '../form-fields/PasswordField/PasswordField'; +import RecaptchaField from '../RecaptchaField/RecaptchaField'; import styles from './RegistrationForm.module.scss'; @@ -33,6 +35,8 @@ type Props = { validationError?: boolean; publisherConsents: CustomFormField[] | null; socialLoginURLs: SocialLoginURLs | null; + captchaSiteKey?: string; + recaptchaRef?: React.RefObject; }; const RegistrationForm: React.FC = ({ @@ -48,6 +52,8 @@ const RegistrationForm: React.FC = ({ consentValues, onConsentChange, consentErrors, + captchaSiteKey, + recaptchaRef, }: Props) => { const { t, i18n } = useTranslation('account'); const location = useLocation(); @@ -131,6 +137,7 @@ const RegistrationForm: React.FC = ({ })} )} + {!!captchaSiteKey && }