diff --git a/package.json b/package.json index 3bd98458..23438425 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react-dom": "^16.12.0", "react-draggable": "^4.2.0", "react-hot-loader": "^4.12.18", + "reaptcha": "^1.7.2", "rxjs": "^6.5.3", "skinview-utils": "^0.2.1", "skinview3d": "^1.2.1", diff --git a/resources/assets/src/components/Captcha.module.scss b/resources/assets/src/components/Captcha.module.scss new file mode 100644 index 00000000..d37c221f --- /dev/null +++ b/resources/assets/src/components/Captcha.module.scss @@ -0,0 +1,3 @@ +.captcha { + cursor: pointer; +} diff --git a/resources/assets/src/components/Captcha.tsx b/resources/assets/src/components/Captcha.tsx new file mode 100644 index 00000000..29099fd4 --- /dev/null +++ b/resources/assets/src/components/Captcha.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import Reaptcha from 'reaptcha' +import { emit, on } from '@/scripts/event' +import { t } from '@/scripts/i18n' +import styles from './Captcha.module.scss' + +const eventId = Symbol() + +type State = { + value: string + time: number + sitekey: string + invisible: boolean +} + +class Captcha extends React.Component<{}, State> { + state: State + ref: React.MutableRefObject + + constructor(props: {}) { + super(props) + this.state = { + value: '', + time: Date.now(), + sitekey: blessing.extra.recaptcha, + invisible: blessing.extra.invisible, + } + this.ref = React.createRef() + } + + execute = async () => { + const recaptcha = this.ref.current + if (recaptcha && this.state.invisible) { + return new Promise(resolve => { + const off = on(eventId, (value: string) => { + resolve(value) + off() + }) + recaptcha.execute() + }) + } + return this.state.value + } + + reset = () => { + const recaptcha = this.ref.current + if (recaptcha) { + recaptcha.reset() + } else { + this.setState({ time: Date.now() }) + } + } + + handleValueChange = (event: React.ChangeEvent) => { + this.setState({ value: event.target.value }) + } + + handleVerify = (value: string) => { + emit(eventId, value) + this.setState({ value }) + } + + handleRefresh = () => { + this.setState({ time: Date.now() }) + } + + render() { + return this.state.sitekey ? ( +
+ +
+ ) : ( +
+
+ +
+ {t('auth.captcha')} +
+ ) + } +} + +export default Captcha diff --git a/resources/assets/tests/__mocks__/reaptcha.tsx b/resources/assets/tests/__mocks__/reaptcha.tsx new file mode 100644 index 00000000..3bbe2b88 --- /dev/null +++ b/resources/assets/tests/__mocks__/reaptcha.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import type { ReaptchaProps } from 'reaptcha' + +class Reaptcha extends React.Component { + execute() { + this.props.onVerify('token') + } + + reset() {} + + render() { + return <> + } +} + +export default Reaptcha diff --git a/resources/assets/tests/components/Captcha.test.tsx b/resources/assets/tests/components/Captcha.test.tsx new file mode 100644 index 00000000..743512a5 --- /dev/null +++ b/resources/assets/tests/components/Captcha.test.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import { render, fireEvent } from '@testing-library/react' +import Reaptcha from 'reaptcha' +import { t } from '@/scripts/i18n' +import Captcha from '@/components/Captcha' + +describe('picture captcha', () => { + it('retrieve value', async () => { + const ref = React.createRef() + const { getByPlaceholderText } = render() + + fireEvent.input(getByPlaceholderText(t('auth.captcha')), { + target: { value: 'abc' }, + }) + expect(await ref.current?.execute()).toBe('abc') + }) + + it('refresh on click', async () => { + const spy = jest.spyOn(Date, 'now') + + const ref = React.createRef() + const { getByAltText } = render() + + fireEvent.click(getByAltText(t('auth.captcha'))) + expect(spy).toBeCalled() + }) + + it('refresh programatically', async () => { + const spy = jest.spyOn(Date, 'now') + + const ref = React.createRef() + render() + + ref.current?.reset() + expect(spy).toBeCalled() + }) +}) + +describe('recaptcha', () => { + beforeEach(() => { + window.blessing.extra = { recaptcha: 'sitekey', invisible: false } + }) + + it('retrieve value', async () => { + window.blessing.extra.invisible = true + const spy = jest.spyOn(Reaptcha.prototype, 'execute') + + const ref = React.createRef() + render() + + const value = await ref.current?.execute() + expect(spy).toBeCalled() + expect(value).toBe('token') + }) + + it('refresh programatically', async () => { + const spy = jest.spyOn(Reaptcha.prototype, 'reset') + + const ref = React.createRef() + render() + + ref.current?.reset() + expect(spy).toBeCalled() + }) +}) diff --git a/yarn.lock b/yarn.lock index ce18a27b..834261a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8571,6 +8571,11 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" +reaptcha@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/reaptcha/-/reaptcha-1.7.2.tgz#d829f54270c241f46501e92a5a7badeb1fcf372d" + integrity sha512-/RXiPeMd+fPUGByv+kAaQlCXCsSflZ9bKX5Fcwv9IYGS1oyT2nntL/8zn9IaiUFHL66T1jBtOABcb92g2+3w8w== + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"