Skip to content

Commit

Permalink
feat(auth): add optional captcha validation to registration
Browse files Browse the repository at this point in the history
  • Loading branch information
royschut committed Nov 1, 2024
1 parent 1ae469b commit 008f1f0
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 7 deletions.
4 changes: 2 additions & 2 deletions packages/common/src/controllers/AccountController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<AuthData> = await this.cleengService.post('/customers', JSON.stringify(payload));
Expand Down
3 changes: 3 additions & 0 deletions packages/common/types/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type LoginArgs = {

export type RegistrationArgs = LoginArgs & {
consents: CustomerConsent[];
captchaValue?: string;
};

export type AuthResponse = {
Expand All @@ -49,6 +50,7 @@ export type LoginFormData = {
export type RegistrationFormData = {
email: string;
password: string;
captchaValue?: string | null;
};

export type ForgotPasswordFormData = {
Expand Down Expand Up @@ -88,6 +90,7 @@ export type RegisterPayload = PayloadWithIPOverride & {
lastName?: string;
externalId?: string;
externalData?: string;
captchaValue?: string;
};

export type CleengCaptureField = {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@use '@jwp/ott-ui-react/src/styles/theme';

.captcha {
width: 100%;
margin-bottom: 8px;
}
18 changes: 18 additions & 0 deletions packages/ui-react/src/components/RecaptchaField/RecaptchaField.tsx
Original file line number Diff line number Diff line change
@@ -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<ReCaptcha, Props>(({ siteKey }, ref) => {
return (
<div className={styles.captcha}>
<ReCaptcha ref={ref} sitekey={siteKey} size={'invisible'} badge="inline" theme="dark" />
</div>
);
});

export default RecaptchaField;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand All @@ -33,6 +35,8 @@ type Props = {
validationError?: boolean;
publisherConsents: CustomFormField[] | null;
socialLoginURLs: SocialLoginURLs | null;
captchaSiteKey?: string;
recaptchaRef?: React.RefObject<ReCAPTCHA>;
};

const RegistrationForm: React.FC<Props> = ({
Expand All @@ -48,6 +52,8 @@ const RegistrationForm: React.FC<Props> = ({
consentValues,
onConsentChange,
consentErrors,
captchaSiteKey,
recaptchaRef,
}: Props) => {
const { t, i18n } = useTranslation('account');
const location = useLocation();
Expand Down Expand Up @@ -131,6 +137,7 @@ const RegistrationForm: React.FC<Props> = ({
})}
</div>
)}
{!!captchaSiteKey && <RecaptchaField siteKey={captchaSiteKey} ref={recaptchaRef} />}
<Button
className={styles.continue}
type="submit"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState, type ChangeEventHandler } from 'react';
import React, { useEffect, useRef, useState, type ChangeEventHandler } from 'react';
import { object, string } from 'yup';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router';
Expand All @@ -10,6 +10,8 @@ import useSocialLoginUrls from '@jwp/ott-hooks-react/src/useSocialLoginUrls';
import useForm from '@jwp/ott-hooks-react/src/useForm';
import { modalURLFromLocation, modalURLFromWindowLocation } from '@jwp/ott-ui-react/src/utils/location';
import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore';
import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore';
import type { ReCAPTCHA } from 'react-google-recaptcha';

import RegistrationForm from '../../../components/RegistrationForm/RegistrationForm';
import { useAriaAnnouncer } from '../../AnnouncementProvider/AnnoucementProvider';
Expand All @@ -30,6 +32,9 @@ const Registration = () => {
loading,
}));

const recaptchaRef = useRef<ReCAPTCHA>(null);
const captchaSiteKey = useConfigStore(({ config }) => (config.custom?.captchaSiteKey ? (config.custom?.captchaSiteKey as string) : undefined));

const handleChangeConsent: ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> = ({ currentTarget }) => {
if (!currentTarget) return;

Expand Down Expand Up @@ -66,6 +71,7 @@ const Registration = () => {
password: string()
.matches(/^(?=.*[a-z])(?=.*[0-9]).{8,}$/, t('registration.invalid_password', { field: t('registration.password') }))
.required(t('registration.field_required', { field: t('registration.password') })),
captchaValue: string().notRequired(),
}),
validateOnBlur: true,
onSubmit: async ({ email, password }) => {
Expand All @@ -76,7 +82,9 @@ const Registration = () => {
throw new Error(t('registration.consents_error'));
}

await accountController.register(email, password, window.location.href, formatConsentsFromValues(publisherConsents, consentValues));
const captchaValue = captchaSiteKey ? (await recaptchaRef.current?.executeAsync()) || undefined : undefined;

await accountController.register(email, password, window.location.href, formatConsentsFromValues(publisherConsents, consentValues), captchaValue);
},
onSubmitSuccess: () => {
announce(t('registration.success'), 'success');
Expand All @@ -99,6 +107,8 @@ const Registration = () => {
publisherConsents={publisherConsents}
loading={loading || publisherConsentsLoading}
socialLoginURLs={socialLoginURLs}
captchaSiteKey={captchaSiteKey}
recaptchaRef={recaptchaRef}
/>
);
};
Expand Down
27 changes: 25 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3902,6 +3902,13 @@
dependencies:
"@types/react" "*"

"@types/react-google-recaptcha@^2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.9.tgz#cd1ffe571fe738473b66690a86dad6c9d3648427"
integrity sha512-nT31LrBDuoSZJN4QuwtQSF3O89FVHC4jLhM+NtKEmVF5R1e8OY0Jo4//x2Yapn2aNHguwgX5doAq8Zo+Ehd0ug==
dependencies:
"@types/react" "*"

"@types/react-helmet@^6.1.11":
version "6.1.11"
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.11.tgz#8cafcafff38f75361f451563ba7b406b0c5d3907"
Expand Down Expand Up @@ -8284,7 +8291,7 @@ hermes-profile-transformer@^0.0.6:
dependencies:
source-map "^0.7.3"

hoist-non-react-statics@^3.3.1:
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
Expand Down Expand Up @@ -11972,7 +11979,7 @@ prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"

prop-types@^15.5.8, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@^15.5.0, prop-types@^15.5.8, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
Expand Down Expand Up @@ -12225,6 +12232,14 @@ react-app-polyfill@^3.0.0:
regenerator-runtime "^0.13.9"
whatwg-fetch "^3.6.2"

react-async-script@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.2.0.tgz#ab9412a26f0b83f5e2e00de1d2befc9400834b21"
integrity sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==
dependencies:
hoist-non-react-statics "^3.3.0"
prop-types "^15.5.0"

react-devtools-core@^4.27.7:
version "4.28.5"
resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.28.5.tgz#c8442b91f068cdf0c899c543907f7f27d79c2508"
Expand All @@ -12246,6 +12261,14 @@ react-fast-compare@^3.1.1:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==

react-google-recaptcha@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz#44aaab834495d922b9d93d7d7a7fb2326315b4ab"
integrity sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==
dependencies:
prop-types "^15.5.0"
react-async-script "^1.2.0"

react-helmet@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
Expand Down

0 comments on commit 008f1f0

Please sign in to comment.