Support reCAPTCHA
This commit is contained in:
parent
f6040707e1
commit
1fa155c213
|
|
@ -233,8 +233,14 @@ class AdminController extends Controller
|
||||||
$form->textarea('meta_extras')->rows(6);
|
$form->textarea('meta_extras')->rows(6);
|
||||||
})->handle();
|
})->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')
|
return view('admin.options')
|
||||||
->with('forms', compact('general', 'announ', 'meta'));
|
->with('forms', compact('general', 'announ', 'meta', 'recaptcha'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resource(Request $request)
|
public function resource(Request $request)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use Session;
|
||||||
use App\Events;
|
use App\Events;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Player;
|
use App\Models\Player;
|
||||||
|
use App\Rules\Captcha;
|
||||||
use App\Mail\ForgotPassword;
|
use App\Mail\ForgotPassword;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
@ -43,7 +44,7 @@ class AuthController extends Controller
|
||||||
$loginFails = (int) Cache::get($loginFailsCacheKey, 0);
|
$loginFails = (int) Cache::get($loginFailsCacheKey, 0);
|
||||||
|
|
||||||
if ($loginFails > 3) {
|
if ($loginFails > 3) {
|
||||||
$this->validate($request, ['captcha' => 'required|captcha']);
|
$this->validate($request, ['captcha' => ['required', new Captcha]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
|
|
@ -83,7 +84,12 @@ class AuthController extends Controller
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
if (option('user_can_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 {
|
} else {
|
||||||
throw new PrettyPageException(trans('auth.register.close'), 7);
|
throw new PrettyPageException(trans('auth.register.close'), 7);
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +107,7 @@ class AuthController extends Controller
|
||||||
$data = $this->validate($request, array_merge([
|
$data = $this->validate($request, array_merge([
|
||||||
'email' => 'required|email|unique:users',
|
'email' => 'required|email|unique:users',
|
||||||
'password' => 'required|min:8|max:32',
|
'password' => 'required|min:8|max:32',
|
||||||
'captcha' => 'required'.(app()->environment('testing') ? '' : '|captcha'),
|
'captcha' => ['required', new Captcha],
|
||||||
], $rule));
|
], $rule));
|
||||||
|
|
||||||
if (option('register_with_player_name')) {
|
if (option('register_with_player_name')) {
|
||||||
|
|
|
||||||
43
app/Rules/Captcha.php
Normal file
43
app/Rules/Captcha.php
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\Rule;
|
||||||
|
|
||||||
|
class Captcha implements Rule
|
||||||
|
{
|
||||||
|
public function passes($attribute, $value)
|
||||||
|
{
|
||||||
|
if (app()->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -50,4 +50,7 @@ return [
|
||||||
'meta_description' => '',
|
'meta_description' => '',
|
||||||
'meta_extras' => '',
|
'meta_extras' => '',
|
||||||
'cdn_address' => '',
|
'cdn_address' => '',
|
||||||
|
'recaptcha_sitekey' => '',
|
||||||
|
'recaptcha_secretkey' => '',
|
||||||
|
'recaptcha_mirror' => 'false',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"toastr": "^2.1.4",
|
"toastr": "^2.1.4",
|
||||||
"vue": "^2.6.9",
|
"vue": "^2.6.9",
|
||||||
"vue-good-table": "^2.16.3",
|
"vue-good-table": "^2.16.3",
|
||||||
|
"vue-recaptcha": "^1.1.1",
|
||||||
"vue-upload-component": "^2.8.19",
|
"vue-upload-component": "^2.8.19",
|
||||||
"vuejs-paginate": "^2.1.0"
|
"vuejs-paginate": "^2.1.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
59
resources/assets/src/components/Captcha.vue
Normal file
59
resources/assets/src/components/Captcha.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="recaptcha" class="row">
|
||||||
|
<div class="col-xs-12" style="padding-bottom: 5px">
|
||||||
|
<vue-recaptcha :sitekey="recaptcha" @verify="$emit('change', $event)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="row">
|
||||||
|
<div class="col-xs-8">
|
||||||
|
<div class="form-group has-feedback">
|
||||||
|
<input
|
||||||
|
ref="captcha"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('auth.captcha')"
|
||||||
|
@input="$emit('change', $event.target.value)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4">
|
||||||
|
<img
|
||||||
|
class="pull-right captcha"
|
||||||
|
:src="`${baseUrl}/auth/captcha?v=${time}`"
|
||||||
|
alt="CAPTCHA"
|
||||||
|
:title="$t('auth.change-captcha')"
|
||||||
|
data-placement="top"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
@click="refreshCaptcha"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VueRecaptcha from 'vue-recaptcha'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Captcha',
|
||||||
|
components: {
|
||||||
|
VueRecaptcha,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
baseUrl: {
|
||||||
|
type: String,
|
||||||
|
default: blessing.base_url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
time: Date.now(),
|
||||||
|
recaptcha: blessing.extra.recaptcha,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refreshCaptcha() {
|
||||||
|
this.time = Date.now()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
12
resources/assets/src/components/mixins/updateCaptcha.ts
Normal file
12
resources/assets/src/components/mixins/updateCaptcha.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
data: () => ({
|
||||||
|
captcha: '',
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
updateCaptcha(value: string) {
|
||||||
|
this.captcha = value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -21,30 +21,7 @@
|
||||||
<span class="glyphicon glyphicon-lock form-control-feedback" />
|
<span class="glyphicon glyphicon-lock form-control-feedback" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="tooManyFails" class="row">
|
<captcha v-if="tooManyFails" ref="captcha" @change="updateCaptcha" />
|
||||||
<div class="col-xs-8">
|
|
||||||
<div class="form-group has-feedback">
|
|
||||||
<input
|
|
||||||
ref="captcha"
|
|
||||||
v-model="captcha"
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
:placeholder="$t('auth.captcha')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-4">
|
|
||||||
<img
|
|
||||||
class="pull-right captcha"
|
|
||||||
:src="`${baseUrl}/auth/captcha?v=${time}`"
|
|
||||||
alt="CAPTCHA"
|
|
||||||
:title="$t('auth.change-captcha')"
|
|
||||||
data-placement="top"
|
|
||||||
data-toggle="tooltip"
|
|
||||||
@click="refreshCaptcha"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="callout callout-info" :class="{ hide: !infoMsg }">{{ infoMsg }}</div>
|
<div class="callout callout-info" :class="{ hide: !infoMsg }">{{ infoMsg }}</div>
|
||||||
<div class="callout callout-warning" :class="{ hide: !warningMsg }">{{ warningMsg }}</div>
|
<div class="callout callout-warning" :class="{ hide: !warningMsg }">{{ warningMsg }}</div>
|
||||||
|
|
@ -81,9 +58,17 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { swal } from '../../js/notify'
|
import { swal } from '../../js/notify'
|
||||||
|
import Captcha from '../../components/Captcha.vue'
|
||||||
|
import updateCaptcha from '../../components/mixins/updateCaptcha'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
|
components: {
|
||||||
|
Captcha,
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
updateCaptcha,
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -96,7 +81,6 @@ export default {
|
||||||
password: '',
|
password: '',
|
||||||
captcha: '',
|
captcha: '',
|
||||||
remember: false,
|
remember: false,
|
||||||
time: Date.now(),
|
|
||||||
tooManyFails: blessing.extra.tooManyFails,
|
tooManyFails: blessing.extra.tooManyFails,
|
||||||
infoMsg: '',
|
infoMsg: '',
|
||||||
warningMsg: '',
|
warningMsg: '',
|
||||||
|
|
@ -121,12 +105,6 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.tooManyFails && !captcha) {
|
|
||||||
this.infoMsg = this.$t('auth.emptyCaptcha')
|
|
||||||
this.$refs.captcha.focus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pending = true
|
this.pending = true
|
||||||
const {
|
const {
|
||||||
errno, msg, login_fails: loginFails,
|
errno, msg, login_fails: loginFails,
|
||||||
|
|
@ -149,15 +127,12 @@ export default {
|
||||||
swal({ type: 'error', text: this.$t('auth.tooManyFails') })
|
swal({ type: 'error', text: this.$t('auth.tooManyFails') })
|
||||||
this.tooManyFails = true
|
this.tooManyFails = true
|
||||||
}
|
}
|
||||||
this.refreshCaptcha()
|
|
||||||
this.infoMsg = ''
|
this.infoMsg = ''
|
||||||
this.warningMsg = msg
|
this.warningMsg = msg
|
||||||
this.pending = false
|
this.pending = false
|
||||||
|
this.$refs.captcha.refreshCaptcha()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshCaptcha() {
|
|
||||||
this.time = Date.now()
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -64,30 +64,7 @@
|
||||||
<span class="glyphicon glyphicon-pencil form-control-feedback" />
|
<span class="glyphicon glyphicon-pencil form-control-feedback" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<captcha ref="captcha" @change="updateCaptcha" />
|
||||||
<div class="col-xs-8">
|
|
||||||
<div class="form-group has-feedback">
|
|
||||||
<input
|
|
||||||
ref="captcha"
|
|
||||||
v-model="captcha"
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
:placeholder="$t('auth.captcha')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-4">
|
|
||||||
<img
|
|
||||||
class="pull-right captcha"
|
|
||||||
:src="`${baseUrl}/auth/captcha?v=${time}`"
|
|
||||||
alt="CAPTCHA"
|
|
||||||
:title="$t('auth.change-captcha')"
|
|
||||||
data-placement="top"
|
|
||||||
data-toggle="tooltip"
|
|
||||||
@click="refreshCaptcha"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="callout callout-info" :class="{ hide: !infoMsg }">{{ infoMsg }}</div>
|
<div class="callout callout-info" :class="{ hide: !infoMsg }">{{ infoMsg }}</div>
|
||||||
<div class="callout callout-warning" :class="{ hide: !warningMsg }">{{ warningMsg }}</div>
|
<div class="callout callout-warning" :class="{ hide: !warningMsg }">{{ warningMsg }}</div>
|
||||||
|
|
@ -114,9 +91,17 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { swal } from '../../js/notify'
|
import { swal } from '../../js/notify'
|
||||||
|
import Captcha from '../../components/Captcha.vue'
|
||||||
|
import updateCaptcha from '../../components/mixins/updateCaptcha'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Register',
|
name: 'Register',
|
||||||
|
components: {
|
||||||
|
Captcha,
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
updateCaptcha,
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -130,7 +115,6 @@ export default {
|
||||||
nickname: '',
|
nickname: '',
|
||||||
playerName: '',
|
playerName: '',
|
||||||
captcha: '',
|
captcha: '',
|
||||||
time: Date.now(),
|
|
||||||
infoMsg: '',
|
infoMsg: '',
|
||||||
warningMsg: '',
|
warningMsg: '',
|
||||||
pending: false,
|
pending: false,
|
||||||
|
|
@ -184,12 +168,6 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!captcha) {
|
|
||||||
this.infoMsg = this.$t('auth.emptyCaptcha')
|
|
||||||
this.$refs.captcha.focus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pending = true
|
this.pending = true
|
||||||
const { errno, msg } = await this.$http.post(
|
const { errno, msg } = await this.$http.post(
|
||||||
'/auth/register',
|
'/auth/register',
|
||||||
|
|
@ -207,13 +185,10 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
this.infoMsg = ''
|
this.infoMsg = ''
|
||||||
this.warningMsg = msg
|
this.warningMsg = msg
|
||||||
this.refreshCaptcha()
|
this.$refs.captcha.refreshCaptcha()
|
||||||
this.pending = false
|
this.pending = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshCaptcha() {
|
|
||||||
this.time = Date.now()
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
23
resources/assets/tests/components/Captcha.test.ts
Normal file
23
resources/assets/tests/components/Captcha.test.ts
Normal file
|
|
@ -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)
|
||||||
|
})
|
||||||
|
|
@ -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')
|
||||||
|
})
|
||||||
|
|
@ -11,14 +11,6 @@ test('show captcha if too many login fails', () => {
|
||||||
expect(wrapper.find('img').attributes('src')).toMatch(/\/auth\/captcha\?v=\d+/)
|
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 () => {
|
test('login', async () => {
|
||||||
window.blessing.extra = { tooManyFails: false }
|
window.blessing.extra = { tooManyFails: false }
|
||||||
Vue.prototype.$http.post
|
Vue.prototype.$http.post
|
||||||
|
|
@ -55,9 +47,6 @@ test('login', async () => {
|
||||||
expect(swal).toBeCalledWith({ type: 'error', text: 'auth.tooManyFails' })
|
expect(swal).toBeCalledWith({ type: 'error', text: 'auth.tooManyFails' })
|
||||||
expect(wrapper.find('img').exists()).toBeTrue()
|
expect(wrapper.find('img').exists()).toBeTrue()
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
expect(info.text()).toBe('auth.emptyCaptcha')
|
|
||||||
|
|
||||||
wrapper.find('[type="text"]').setValue('a')
|
wrapper.find('[type="text"]').setValue('a')
|
||||||
wrapper.find('[type="checkbox"]').setChecked()
|
wrapper.find('[type="checkbox"]').setChecked()
|
||||||
button.trigger('click')
|
button.trigger('click')
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,6 @@ jest.mock('@/js/notify')
|
||||||
|
|
||||||
window.blessing.extra = { player: false }
|
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', () => {
|
test('require player name', () => {
|
||||||
window.blessing.extra = { player: true }
|
window.blessing.extra = { player: true }
|
||||||
|
|
||||||
|
|
@ -74,10 +67,6 @@ test('register', async () => {
|
||||||
|
|
||||||
wrapper.findAll('[type="text"]').at(0)
|
wrapper.findAll('[type="text"]').at(0)
|
||||||
.setValue('abc')
|
.setValue('abc')
|
||||||
button.trigger('click')
|
|
||||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
|
||||||
expect(info.text()).toBe('auth.emptyCaptcha')
|
|
||||||
|
|
||||||
wrapper.findAll('[type="text"]').at(1)
|
wrapper.findAll('[type="text"]').at(1)
|
||||||
.setValue('captcha')
|
.setValue('captcha')
|
||||||
button.trigger('click')
|
button.trigger('click')
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,11 @@ meta:
|
||||||
meta_extras:
|
meta_extras:
|
||||||
title: Other Custom <meta> Tags
|
title: Other Custom <meta> 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!
|
res-warning: This page is ONLY for advanced users. If you aren't familiar with these, please don't modify them!
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ min:
|
||||||
not_in: 'The selected :attribute is invalid.'
|
not_in: 'The selected :attribute is invalid.'
|
||||||
numeric: 'The :attribute must be a number.'
|
numeric: 'The :attribute must be a number.'
|
||||||
present: 'The :attribute field must be present.'
|
present: 'The :attribute field must be present.'
|
||||||
|
recaptcha: 'reCAPTCHA validation failed.'
|
||||||
regex: 'The :attribute format is invalid.'
|
regex: 'The :attribute format is invalid.'
|
||||||
required: 'The :attribute field is required.'
|
required: 'The :attribute field is required.'
|
||||||
required_if: 'The :attribute field is required when :other is :value.'
|
required_if: 'The :attribute field is required when :other is :value.'
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,11 @@ meta:
|
||||||
meta_extras:
|
meta_extras:
|
||||||
title: 其它自定义 <meta> 标签
|
title: 其它自定义 <meta> 标签
|
||||||
|
|
||||||
|
recaptcha:
|
||||||
|
recaptcha_mirror:
|
||||||
|
title: 第三方镜像
|
||||||
|
label: 国内用户请勾选此项
|
||||||
|
|
||||||
res-warning: 本页面仅供高级用户使用。如果您不清楚这些设置的含义,请不要随意修改它们!
|
res-warning: 本页面仅供高级用户使用。如果您不清楚这些设置的含义,请不要随意修改它们!
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ min:
|
||||||
not_in: '已选的属性 :attribute 非法。'
|
not_in: '已选的属性 :attribute 非法。'
|
||||||
numeric: ':attribute 必须是一个数字。'
|
numeric: ':attribute 必须是一个数字。'
|
||||||
present: ':attribute 必须存在。'
|
present: ':attribute 必须存在。'
|
||||||
|
recaptcha: '未能通过 reCAPTCHA 的验证。'
|
||||||
regex: ':attribute 格式不正确。'
|
regex: ':attribute 格式不正确。'
|
||||||
required: ':attribute 不能为空。'
|
required: ':attribute 不能为空。'
|
||||||
required_if: '当 :other 为 :value 时 :attribute 不能为空。'
|
required_if: '当 :other 为 :value 时 :attribute 不能为空。'
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@
|
||||||
{!! $forms['announ']->render() !!}
|
{!! $forms['announ']->render() !!}
|
||||||
|
|
||||||
{!! $forms['meta']->render() !!}
|
{!! $forms['meta']->render() !!}
|
||||||
|
|
||||||
|
{!! $forms['recaptcha']->render() !!}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,14 @@
|
||||||
<!-- /.login-box-body -->
|
<!-- /.login-box-body -->
|
||||||
</div>
|
</div>
|
||||||
<!-- /.login-box -->
|
<!-- /.login-box -->
|
||||||
|
@include('common.recaptcha')
|
||||||
<script>
|
<script>
|
||||||
Object.defineProperty(blessing, 'extra', {
|
Object.defineProperty(blessing, 'extra', {
|
||||||
configurable: false,
|
configurable: false,
|
||||||
get: () => Object.freeze(@json(['tooManyFails' => cache(sha1('login_fails_'.get_client_ip())) > 3]))
|
get: () => Object.freeze(@json([
|
||||||
|
'tooManyFails' => cache(sha1('login_fails_'.get_client_ip())) > 3,
|
||||||
|
'recaptcha' => option('recaptcha_sitekey'),
|
||||||
|
]))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
<!-- /.form-box -->
|
<!-- /.form-box -->
|
||||||
</div>
|
</div>
|
||||||
<!-- /.register-box -->
|
<!-- /.register-box -->
|
||||||
|
@include('common.recaptcha')
|
||||||
<script>
|
<script>
|
||||||
Object.defineProperty(blessing, 'extra', {
|
Object.defineProperty(blessing, 'extra', {
|
||||||
get: () => Object.freeze(@json($extra)),
|
get: () => Object.freeze(@json($extra)),
|
||||||
|
|
|
||||||
11
resources/views/common/recaptcha.blade.php
Normal file
11
resources/views/common/recaptcha.blade.php
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
@if (option('recaptcha_sitekey'))
|
||||||
|
<script
|
||||||
|
src="https://www.{{ option('recaptcha_mirror')
|
||||||
|
? 'recaptcha.net'
|
||||||
|
: 'google.com'
|
||||||
|
}}/recaptcha/api.js?onload=vueRecaptchaApiLoaded&render=explicit"
|
||||||
|
async
|
||||||
|
defer
|
||||||
|
>
|
||||||
|
</script>
|
||||||
|
@endif
|
||||||
|
|
@ -159,6 +159,15 @@ class AdminControllerTest extends BrowserKitTestCase
|
||||||
->see('<meta name="keywords" content="kw">')
|
->see('<meta name="keywords" content="kw">')
|
||||||
->see('<meta name="description" content="desc">')
|
->see('<meta name="description" content="desc">')
|
||||||
->see('<!-- nothing -->');
|
->see('<!-- nothing -->');
|
||||||
|
|
||||||
|
$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()
|
public function testResource()
|
||||||
|
|
|
||||||
|
|
@ -8557,6 +8557,11 @@ vue-loader@^15.7.0:
|
||||||
vue-hot-reload-api "^2.3.0"
|
vue-hot-reload-api "^2.3.0"
|
||||||
vue-style-loader "^4.1.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:
|
vue-style-loader@^4.1.0:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8"
|
resolved "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user