diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php
index 8b0faa02..7a694e2a 100644
--- a/app/Http/Controllers/AdminController.php
+++ b/app/Http/Controllers/AdminController.php
@@ -233,8 +233,14 @@ class AdminController extends Controller
$form->textarea('meta_extras')->rows(6);
})->handle();
+ $recaptcha = Option::form('recaptcha', 'reCAPTCHA', function ($form) {
+ $form->text('recaptcha_sitekey', 'sitekey');
+ $form->text('recaptcha_secretkey', 'secretkey');
+ $form->checkbox('recaptcha_mirror')->label();
+ })->handle();
+
return view('admin.options')
- ->with('forms', compact('general', 'announ', 'meta'));
+ ->with('forms', compact('general', 'announ', 'meta', 'recaptcha'));
}
public function resource(Request $request)
diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php
index fe75e91e..ce0de1c3 100644
--- a/app/Http/Controllers/AuthController.php
+++ b/app/Http/Controllers/AuthController.php
@@ -10,6 +10,7 @@ use Session;
use App\Events;
use App\Models\User;
use App\Models\Player;
+use App\Rules\Captcha;
use App\Mail\ForgotPassword;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -43,7 +44,7 @@ class AuthController extends Controller
$loginFails = (int) Cache::get($loginFailsCacheKey, 0);
if ($loginFails > 3) {
- $this->validate($request, ['captcha' => 'required|captcha']);
+ $this->validate($request, ['captcha' => ['required', new Captcha]]);
}
if (! $user) {
@@ -83,7 +84,12 @@ class AuthController extends Controller
public function register()
{
if (option('user_can_register')) {
- return view('auth.register', ['extra' => ['player' => option('register_with_player_name')]]);
+ return view('auth.register', [
+ 'extra' => [
+ 'player' => option('register_with_player_name'),
+ 'recaptcha' => option('recaptcha_sitekey'),
+ ]
+ ]);
} else {
throw new PrettyPageException(trans('auth.register.close'), 7);
}
@@ -101,7 +107,7 @@ class AuthController extends Controller
$data = $this->validate($request, array_merge([
'email' => 'required|email|unique:users',
'password' => 'required|min:8|max:32',
- 'captcha' => 'required'.(app()->environment('testing') ? '' : '|captcha'),
+ 'captcha' => ['required', new Captcha],
], $rule));
if (option('register_with_player_name')) {
diff --git a/app/Rules/Captcha.php b/app/Rules/Captcha.php
new file mode 100644
index 00000000..9a1e4190
--- /dev/null
+++ b/app/Rules/Captcha.php
@@ -0,0 +1,43 @@
+environment('testing')) {
+ return true;
+ }
+
+ $secretkey = option('recaptcha_secretkey');
+ if ($secretkey) {
+ $client = new \GuzzleHttp\Client();
+ $url = option('recaptcha_mirror')
+ ? 'https://www.recaptcha.net/recaptcha/api/siteverify'
+ : 'https://www.google.com/recaptcha/api/siteverify';
+ $response = $client->post($url, [
+ 'form_params' => [
+ 'secret' => $secretkey,
+ 'response' => $value,
+ ]
+ ]);
+ if ($response->getStatusCode() == 200) {
+ $body = json_decode((string) $response->getBody());
+ return $body->success;
+ }
+ return false;
+ }
+
+ return captcha_check($value);
+ }
+
+ public function message()
+ {
+ return option('recaptcha_secretkey')
+ ? trans('validation.recaptcha')
+ : trans('validation.captcha');
+ }
+}
diff --git a/config/options.php b/config/options.php
index ac9c7eb6..a36ff41c 100644
--- a/config/options.php
+++ b/config/options.php
@@ -50,4 +50,7 @@ return [
'meta_description' => '',
'meta_extras' => '',
'cdn_address' => '',
+ 'recaptcha_sitekey' => '',
+ 'recaptcha_secretkey' => '',
+ 'recaptcha_mirror' => 'false',
];
diff --git a/package.json b/package.json
index a71c57e5..bcecde8b 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"toastr": "^2.1.4",
"vue": "^2.6.9",
"vue-good-table": "^2.16.3",
+ "vue-recaptcha": "^1.1.1",
"vue-upload-component": "^2.8.19",
"vuejs-paginate": "^2.1.0"
},
diff --git a/resources/assets/src/components/Captcha.vue b/resources/assets/src/components/Captcha.vue
new file mode 100644
index 00000000..18b04eb4
--- /dev/null
+++ b/resources/assets/src/components/Captcha.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
![CAPTCHA]()
+
+
+
+
+
diff --git a/resources/assets/src/components/mixins/updateCaptcha.ts b/resources/assets/src/components/mixins/updateCaptcha.ts
new file mode 100644
index 00000000..b74fb54e
--- /dev/null
+++ b/resources/assets/src/components/mixins/updateCaptcha.ts
@@ -0,0 +1,12 @@
+import Vue from 'vue'
+
+export default Vue.extend({
+ data: () => ({
+ captcha: '',
+ }),
+ methods: {
+ updateCaptcha(value: string) {
+ this.captcha = value
+ },
+ },
+})
diff --git a/resources/assets/src/views/auth/Login.vue b/resources/assets/src/views/auth/Login.vue
index c4dcf0df..52617def 100644
--- a/resources/assets/src/views/auth/Login.vue
+++ b/resources/assets/src/views/auth/Login.vue
@@ -21,30 +21,7 @@
-
-
-
-
![CAPTCHA]()
-
-
+
{{ infoMsg }}
{{ warningMsg }}
@@ -81,9 +58,17 @@
diff --git a/resources/assets/src/views/auth/Register.vue b/resources/assets/src/views/auth/Register.vue
index 20a62648..7dde105b 100644
--- a/resources/assets/src/views/auth/Register.vue
+++ b/resources/assets/src/views/auth/Register.vue
@@ -64,30 +64,7 @@
-
-
-
-
![CAPTCHA]()
-
-
+
{{ infoMsg }}
{{ warningMsg }}
@@ -114,9 +91,17 @@
diff --git a/resources/assets/tests/components/Captcha.test.ts b/resources/assets/tests/components/Captcha.test.ts
new file mode 100644
index 00000000..806aa78e
--- /dev/null
+++ b/resources/assets/tests/components/Captcha.test.ts
@@ -0,0 +1,23 @@
+import { mount } from '@vue/test-utils'
+import Captcha from '@/components/Captcha.vue'
+
+test('display recaptcha', () => {
+ blessing.extra = { recaptcha: 'sitekey' }
+ const wrapper = mount(Captcha)
+ expect(wrapper.find('img').exists()).toBeFalse()
+})
+
+test('display characters captcha', () => {
+ blessing.extra = {}
+ const wrapper = mount(Captcha)
+ expect(wrapper.find('img').exists()).toBeTrue()
+ wrapper.find('input').setValue('abc')
+ expect(wrapper.emitted().change[0][0]).toBe('abc')
+})
+
+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/components/mixins/updateCaptcha.test.ts b/resources/assets/tests/components/mixins/updateCaptcha.test.ts
new file mode 100644
index 00000000..6e77ac22
--- /dev/null
+++ b/resources/assets/tests/components/mixins/updateCaptcha.test.ts
@@ -0,0 +1,8 @@
+import { mount } from '@vue/test-utils'
+import updateCaptcha from '@/components/mixins/updateCaptcha'
+
+test('update captcha', () => {
+ const wrapper = mount(updateCaptcha)
+ wrapper.vm.updateCaptcha('value')
+ expect(wrapper.vm.captcha).toBe('value')
+})
diff --git a/resources/assets/tests/views/auth/Login.test.ts b/resources/assets/tests/views/auth/Login.test.ts
index 7d440582..7460605f 100644
--- a/resources/assets/tests/views/auth/Login.test.ts
+++ b/resources/assets/tests/views/auth/Login.test.ts
@@ -11,14 +11,6 @@ test('show captcha if too many login fails', () => {
expect(wrapper.find('img').attributes('src')).toMatch(/\/auth\/captcha\?v=\d+/)
})
-test('click to refresh captcha', () => {
- window.blessing.extra = { tooManyFails: true }
- jest.spyOn(Date, 'now')
- const wrapper = mount(Login)
- wrapper.find('img').trigger('click')
- expect(Date.now).toBeCalledTimes(2)
-})
-
test('login', async () => {
window.blessing.extra = { tooManyFails: false }
Vue.prototype.$http.post
@@ -55,9 +47,6 @@ test('login', async () => {
expect(swal).toBeCalledWith({ type: 'error', text: 'auth.tooManyFails' })
expect(wrapper.find('img').exists()).toBeTrue()
- button.trigger('click')
- expect(info.text()).toBe('auth.emptyCaptcha')
-
wrapper.find('[type="text"]').setValue('a')
wrapper.find('[type="checkbox"]').setChecked()
button.trigger('click')
diff --git a/resources/assets/tests/views/auth/Register.test.ts b/resources/assets/tests/views/auth/Register.test.ts
index a5fdf52b..12e2858c 100644
--- a/resources/assets/tests/views/auth/Register.test.ts
+++ b/resources/assets/tests/views/auth/Register.test.ts
@@ -7,13 +7,6 @@ jest.mock('@/js/notify')
window.blessing.extra = { player: false }
-test('click to refresh captcha', () => {
- jest.spyOn(Date, 'now')
- const wrapper = mount(Register)
- wrapper.find('img').trigger('click')
- expect(Date.now).toBeCalledTimes(2)
-})
-
test('require player name', () => {
window.blessing.extra = { player: true }
@@ -74,10 +67,6 @@ test('register', async () => {
wrapper.findAll('[type="text"]').at(0)
.setValue('abc')
- button.trigger('click')
- expect(Vue.prototype.$http.post).not.toBeCalled()
- expect(info.text()).toBe('auth.emptyCaptcha')
-
wrapper.findAll('[type="text"]').at(1)
.setValue('captcha')
button.trigger('click')
diff --git a/resources/lang/en/options.yml b/resources/lang/en/options.yml
index 627462dc..e648a277 100644
--- a/resources/lang/en/options.yml
+++ b/resources/lang/en/options.yml
@@ -146,6 +146,11 @@ meta:
meta_extras:
title: Other Custom Tags
+recaptcha:
+ recaptcha_mirror:
+ title: 3rd-party Mirror
+ label: Chinese users only.
+
res-warning: This page is ONLY for advanced users. If you aren't familiar with these, please don't modify them!
resources:
diff --git a/resources/lang/en/validation.yml b/resources/lang/en/validation.yml
index b65c4699..74964d40 100644
--- a/resources/lang/en/validation.yml
+++ b/resources/lang/en/validation.yml
@@ -49,6 +49,7 @@ min:
not_in: 'The selected :attribute is invalid.'
numeric: 'The :attribute must be a number.'
present: 'The :attribute field must be present.'
+recaptcha: 'reCAPTCHA validation failed.'
regex: 'The :attribute format is invalid.'
required: 'The :attribute field is required.'
required_if: 'The :attribute field is required when :other is :value.'
diff --git a/resources/lang/zh_CN/options.yml b/resources/lang/zh_CN/options.yml
index c7a4bead..c44b84c5 100644
--- a/resources/lang/zh_CN/options.yml
+++ b/resources/lang/zh_CN/options.yml
@@ -146,6 +146,11 @@ meta:
meta_extras:
title: 其它自定义 标签
+recaptcha:
+ recaptcha_mirror:
+ title: 第三方镜像
+ label: 国内用户请勾选此项
+
res-warning: 本页面仅供高级用户使用。如果您不清楚这些设置的含义,请不要随意修改它们!
resources:
diff --git a/resources/lang/zh_CN/validation.yml b/resources/lang/zh_CN/validation.yml
index 942900d5..f5bf11ab 100644
--- a/resources/lang/zh_CN/validation.yml
+++ b/resources/lang/zh_CN/validation.yml
@@ -52,6 +52,7 @@ min:
not_in: '已选的属性 :attribute 非法。'
numeric: ':attribute 必须是一个数字。'
present: ':attribute 必须存在。'
+recaptcha: '未能通过 reCAPTCHA 的验证。'
regex: ':attribute 格式不正确。'
required: ':attribute 不能为空。'
required_if: '当 :other 为 :value 时 :attribute 不能为空。'
diff --git a/resources/views/admin/options.blade.php b/resources/views/admin/options.blade.php
index 32c13a98..52e681a2 100644
--- a/resources/views/admin/options.blade.php
+++ b/resources/views/admin/options.blade.php
@@ -25,6 +25,8 @@
{!! $forms['announ']->render() !!}
{!! $forms['meta']->render() !!}
+
+ {!! $forms['recaptcha']->render() !!}
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php
index 76511fd4..da920dc5 100644
--- a/resources/views/auth/login.blade.php
+++ b/resources/views/auth/login.blade.php
@@ -23,11 +23,14 @@
-
+@include('common.recaptcha')
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php
index 11953560..19ca5ff3 100644
--- a/resources/views/auth/register.blade.php
+++ b/resources/views/auth/register.blade.php
@@ -17,7 +17,7 @@
-
+@include('common.recaptcha')
+@endif
diff --git a/tests/AdminControllerTest.php b/tests/AdminControllerTest.php
index 13d1c2ce..4e0ee031 100644
--- a/tests/AdminControllerTest.php
+++ b/tests/AdminControllerTest.php
@@ -159,6 +159,15 @@ class AdminControllerTest extends BrowserKitTestCase
->see('')
->see('')
->see('');
+
+ $this->visit('/admin/options')
+ ->type('key', 'recaptcha_sitekey')
+ ->type('secret', 'recaptcha_secretkey')
+ ->check('recaptcha_mirror')
+ ->press('submit_recaptcha');
+ $this->assertEquals('key', option('recaptcha_sitekey'));
+ $this->assertEquals('secret', option('recaptcha_secretkey'));
+ $this->assertTrue(option('recaptcha_mirror'));
}
public function testResource()
diff --git a/yarn.lock b/yarn.lock
index e36b61ad..aab110d4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8557,6 +8557,11 @@ vue-loader@^15.7.0:
vue-hot-reload-api "^2.3.0"
vue-style-loader "^4.1.0"
+vue-recaptcha@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/vue-recaptcha/-/vue-recaptcha-1.1.1.tgz#be38f4ffab500e7f4775f149a279bb962cf91f07"
+ integrity sha512-ThjujwmoLrDS6EeE7AGHmeGfWDhFOFUAyfPfSCXojYMoSwFOJv0LL0/86PqHwjHM69HIq0kBI9ps+JeIQa5V/A==
+
vue-style-loader@^4.1.0:
version "4.1.2"
resolved "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8"