diff --git a/app/Providers/ViewServiceProvider.php b/app/Providers/ViewServiceProvider.php index 7eb2c09e..3918a480 100644 --- a/app/Providers/ViewServiceProvider.php +++ b/app/Providers/ViewServiceProvider.php @@ -65,15 +65,6 @@ class ViewServiceProvider extends ServiceProvider View::composer('shared.foot', Composers\FootComposer::class); - View::composer('auth.*', function ($view) { - $view->with('enable_recaptcha', (bool) option('recaptcha_sitekey')); - $view->with( - 'recaptcha_url', - 'https://www.recaptcha.net/recaptcha/api.js' - .'?onload=vueRecaptchaApiLoaded&render=explicit' - ); - }); - View::composer(['errors.*', 'setup.*'], function ($view) use ($webpack) { $view->with([ 'styles' => [ diff --git a/package.json b/package.json index bc1fef63..a2bcc826 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "use-immer": "^0.3.5", "vue": "^2.6.11", "vue-good-table": "^2.18.1", - "vue-recaptcha": "^1.2.0", "xterm": "^4.4.0", "xterm-addon-fit": "^0.3.0" }, diff --git a/resources/assets/src/components/Captcha.vue b/resources/assets/src/components/Captcha.vue deleted file mode 100644 index eb216c66..00000000 --- a/resources/assets/src/components/Captcha.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - diff --git a/resources/assets/src/scripts/route.tsx b/resources/assets/src/scripts/route.tsx index c6977092..3e0756a4 100644 --- a/resources/assets/src/scripts/route.tsx +++ b/resources/assets/src/scripts/route.tsx @@ -96,8 +96,8 @@ export default [ }, { path: 'auth/register', - component: () => import('../views/auth/Register.vue'), - el: 'form', + react: () => import('../views/auth/Registration'), + el: 'main', }, { path: 'auth/forgot', diff --git a/resources/assets/src/views/auth/Register.vue b/resources/assets/src/views/auth/Register.vue deleted file mode 100644 index df7faef4..00000000 --- a/resources/assets/src/views/auth/Register.vue +++ /dev/null @@ -1,186 +0,0 @@ - - - diff --git a/resources/assets/src/views/auth/Registration.tsx b/resources/assets/src/views/auth/Registration.tsx new file mode 100644 index 00000000..1961e37b --- /dev/null +++ b/resources/assets/src/views/auth/Registration.tsx @@ -0,0 +1,182 @@ +import React, { useState, useRef } from 'react' +import { hot } from 'react-hot-loader/root' +import useBlessingExtra from '@/scripts/hooks/useBlessingExtra' +import { t } from '@/scripts/i18n' +import * as fetch from '@/scripts/net' +import { toast } from '@/scripts/notify' +import Alert from '@/components/Alert' +import Captcha from '@/components/Captcha' + +const Registration: React.FC = () => { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [confirmation, setConfirmation] = useState('') + const [nickName, setNickName] = useState('') + const [playerName, setPlayerName] = useState('') + const [isPending, setIsPending] = useState(false) + const [warningMessage, setWarningMessage] = useState('') + const requirePlayer = useBlessingExtra('player') + const confirmationRef = useRef(null) + const captchaRef = useRef(null) + + const handleEmailChange = (event: React.ChangeEvent) => { + setEmail(event.target.value) + } + + const handlePasswordChange = (event: React.ChangeEvent) => { + setPassword(event.target.value) + } + + const handleConfirmationChange = ( + event: React.ChangeEvent, + ) => { + setConfirmation(event.target.value) + } + + const handleNickNameChange = (event: React.ChangeEvent) => { + setNickName(event.target.value) + } + + const handlePlayerNameChange = ( + event: React.ChangeEvent, + ) => { + setPlayerName(event.target.value) + } + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault() + setWarningMessage('') + + if (password !== confirmation) { + setWarningMessage(t('auth.invalidConfirmPwd')) + confirmationRef.current!.focus() + return + } + + setIsPending(true) + const { code, message } = await fetch.post( + '/auth/register', + Object.assign( + { email, password, captcha: await captchaRef.current!.execute() }, + requirePlayer ? { player_name: playerName } : { nickname: nickName }, + ), + ) + if (code === 0) { + toast.success(message) + setTimeout(() => { + window.location.href = `${blessing.base_url}/user` + }, 3000) + } else { + setWarningMessage(message) + captchaRef.current!.reset() + } + setIsPending(false) + } + + return ( +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ {requirePlayer ? ( +
+ +
+
+ +
+
+
+ ) : ( +
+ +
+
+ +
+
+
+ )} + + + {warningMessage} + +
+ {t('auth.login-link')} + +
+ + ) +} + +export default hot(Registration) diff --git a/resources/assets/tests/components/Captcha.test.ts b/resources/assets/tests/components/Captcha.test.ts deleted file mode 100644 index 0d26f638..00000000 --- a/resources/assets/tests/components/Captcha.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import Vue from 'vue' -import { mount } from '@vue/test-utils' -import Captcha from '@/components/Captcha.vue' - -const VueRecaptcha = Vue.extend({ - methods: { - execute() { - this.$emit('verify', 'value') - }, - }, -}) - -test('display recaptcha', () => { - blessing.extra = { recaptcha: 'sitekey' } - const wrapper = mount(Captcha) - expect(wrapper.find('img').exists()).toBeFalse() -}) - -test('refresh recaptcha', () => { - const wrapper = mount(Captcha) - wrapper.vm.refresh() -}) - -test('recaptcha verified', () => { - const wrapper = - mount(Captcha) - wrapper.vm.onVerify('value') - expect(wrapper.vm.value).toBe('value') -}) - -test('invoke recaptcha', async () => { - const wrapper = mount }>(Captcha, { stubs: { VueRecaptcha } }) - wrapper.setData({ invisible: true }) - expect(await wrapper.vm.execute()).toBe('value') - - wrapper.setData({ invisible: false, value: 'haha' }) - expect(await wrapper.vm.execute()).toBe('haha') -}) - -test('display characters captcha', async () => { - blessing.extra = {} - const wrapper = mount }>(Captcha) - expect(wrapper.find('img').exists()).toBeTrue() - const input = wrapper.find('input') - input.setValue('abc') - expect(await wrapper.vm.execute()).toBe('abc') - - wrapper.setData({ invisible: true }) - input.setValue('123') - expect(await wrapper.vm.execute()).toBe('123') -}) - -test('refresh captcha', () => { - jest.spyOn(Date, 'now') - const wrapper = mount(Captcha) - wrapper.find('img').trigger('click') - expect(Date.now).toBeCalledTimes(2) -}) diff --git a/resources/assets/tests/views/auth/Register.test.ts b/resources/assets/tests/views/auth/Register.test.ts deleted file mode 100644 index f9b1c5b2..00000000 --- a/resources/assets/tests/views/auth/Register.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import Vue from 'vue' -import { mount } from '@vue/test-utils' -import { flushPromises } from '../../utils' -import { toast } from '@/scripts/notify' -import Register from '@/views/auth/Register.vue' - -jest.mock('@/scripts/notify') - -window.blessing.extra = { player: false } - -const Captcha = Vue.extend({ - methods: { - execute() { - return Promise.resolve('captcha') - }, - refresh() { /* */ }, - }, -}) - -test('require player name', () => { - window.blessing.extra = { player: true } - - const wrapper = mount(Register) - expect(wrapper.findAll('[type="text"]').at(0) - .attributes('placeholder')).toBe('auth.player-name') - - window.blessing.extra = { player: false } -}) - -test('register', async () => { - jest.spyOn(Date, 'now') - Vue.prototype.$http.post - .mockResolvedValueOnce({ code: 1, message: 'fail' }) - .mockResolvedValueOnce({ code: 0, message: 'ok' }) - const wrapper = mount(Register, { stubs: { Captcha } }) - const form = wrapper.find('form') - const info = wrapper.find('.alert-info') - const warning = wrapper.find('.alert-warning') - - wrapper.find('[type="email"]').setValue('a@b.c') - wrapper.findAll('[type="password"]').at(0) - .setValue('12345678') - wrapper.findAll('[type="password"]').at(1) - .setValue('123456') - form.trigger('submit') - expect(Vue.prototype.$http.post).not.toBeCalled() - expect(info.text()).toBe('auth.invalidConfirmPwd') - - wrapper.findAll('[type="password"]').at(1) - .setValue('12345678') - wrapper.findAll('[type="text"]').at(0) - .setValue('abc') - form.trigger('submit') - await flushPromises() - expect(Vue.prototype.$http.post).toBeCalledWith( - '/auth/register', - { - email: 'a@b.c', - password: '12345678', - nickname: 'abc', - captcha: 'captcha', - }, - ) - await flushPromises() - expect(warning.text()).toBe('fail') - - form.trigger('submit') - await flushPromises() - jest.runAllTimers() - expect(toast.success).toBeCalledWith('ok') -}) - -test('register with player name', async () => { - window.blessing.extra = { player: true } - Vue.prototype.$http.post.mockResolvedValue({ code: 0, message: 'ok' }) - const wrapper = mount(Register, { stubs: { Captcha } }) - const form = wrapper.find('form') - wrapper.find('[type="email"]').setValue('a@b.c') - wrapper.findAll('[type="password"]').at(0) - .setValue('12345678') - wrapper.findAll('[type="password"]').at(1) - .setValue('12345678') - wrapper.findAll('[type="text"]').at(0) - .setValue('abc') - form.trigger('submit') - await flushPromises() - expect(Vue.prototype.$http.post).toBeCalledWith( - '/auth/register', - { - email: 'a@b.c', - password: '12345678', - player_name: 'abc', - captcha: 'captcha', - }, - ) -}) diff --git a/resources/assets/tests/views/auth/Registration.test.tsx b/resources/assets/tests/views/auth/Registration.test.tsx new file mode 100644 index 00000000..c6b5c53d --- /dev/null +++ b/resources/assets/tests/views/auth/Registration.test.tsx @@ -0,0 +1,139 @@ +import React from 'react' +import { render, waitFor, fireEvent } from '@testing-library/react' +import { t } from '@/scripts/i18n' +import * as fetch from '@/scripts/net' +import Registration from '@/views/auth/Registration' + +jest.mock('@/scripts/net') + +beforeEach(() => { + window.blessing.extra = { player: false } +}) + +test('confirmation is not matched', () => { + const { getByText, getByPlaceholderText, queryByText } = render( + , + ) + + fireEvent.input(getByPlaceholderText(t('auth.email')), { + target: { value: 'a@b.c' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.password')), { + target: { value: 'password' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.repeat-pwd')), { + target: { value: 'password1' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.nickname')), { + target: { value: 't' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.captcha')), { + target: { value: 'a' }, + }) + fireEvent.click(getByText(t('auth.register-button'))) + + expect(queryByText(t('auth.invalidConfirmPwd'))).toBeInTheDocument() + expect(fetch.post).not.toBeCalled() +}) + +test('succeeded', async () => { + fetch.post.mockResolvedValue({ code: 0, message: 'ok' }) + const { getByText, getByPlaceholderText, getByRole, queryByText } = render( + , + ) + + fireEvent.input(getByPlaceholderText(t('auth.email')), { + target: { value: 'a@b.c' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.password')), { + target: { value: 'password' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.repeat-pwd')), { + target: { value: 'password' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.nickname')), { + target: { value: 't' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.captcha')), { + target: { value: 'a' }, + }) + fireEvent.click(getByText(t('auth.register-button'))) + await waitFor(() => + expect(fetch.post).toBeCalledWith('/auth/register', { + email: 'a@b.c', + password: 'password', + nickname: 't', + captcha: 'a', + }), + ) + expect(queryByText('ok')).toBeInTheDocument() + expect(getByRole('status')).toHaveClass('alert-success') + jest.runAllTimers() +}) + +test('failed', async () => { + fetch.post.mockResolvedValue({ code: 1, message: 'failed' }) + const { getByText, getByPlaceholderText, queryByText } = render( + , + ) + + fireEvent.input(getByPlaceholderText(t('auth.email')), { + target: { value: 'a@b.c' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.password')), { + target: { value: 'password' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.repeat-pwd')), { + target: { value: 'password' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.nickname')), { + target: { value: 't' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.captcha')), { + target: { value: 'a' }, + }) + fireEvent.click(getByText(t('auth.register-button'))) + await waitFor(() => + expect(fetch.post).toBeCalledWith('/auth/register', { + email: 'a@b.c', + password: 'password', + nickname: 't', + captcha: 'a', + }), + ) + expect(queryByText('failed')).toBeInTheDocument() +}) + +test('register with new player', async () => { + window.blessing.extra = { player: true } + fetch.post.mockResolvedValue({ code: 0, message: 'ok' }) + const { getByText, getByPlaceholderText, queryByText } = render( + , + ) + + fireEvent.input(getByPlaceholderText(t('auth.email')), { + target: { value: 'a@b.c' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.password')), { + target: { value: 'password' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.repeat-pwd')), { + target: { value: 'password' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.player-name')), { + target: { value: 'player' }, + }) + fireEvent.input(getByPlaceholderText(t('auth.captcha')), { + target: { value: 'a' }, + }) + fireEvent.click(getByText(t('auth.register-button'))) + await waitFor(() => + expect(fetch.post).toBeCalledWith('/auth/register', { + email: 'a@b.c', + password: 'password', + player_name: 'player', + captcha: 'a', + }), + ) + expect(queryByText('ok')).toBeInTheDocument() +}) diff --git a/resources/views/auth/register.twig b/resources/views/auth/register.twig index 0e414cef..1bb71d73 100644 --- a/resources/views/auth/register.twig +++ b/resources/views/auth/register.twig @@ -6,14 +6,11 @@ -
+
{{ include('auth.oauth') }} {% endblock %} {% block before_foot %} - {% if enable_recaptcha %} - - {% endif %}