Merge remote-tracking branch 'origin/recaptcha'

# Conflicts:
#	public/mix-manifest.json
#	src/Domain/Settings/Controllers/StoreSocialServiceCredentialsController.php
This commit is contained in:
Čarodej
2022-01-25 16:29:37 +01:00
17 changed files with 223 additions and 10 deletions

View File

@@ -68,6 +68,9 @@ GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
RECAPTCHA_CLIENT_ID=
RECAPTCHA_CLIENT_SECRET=
SANCTUM_STATEFUL_DOMAINS=localhost,localhost:8000,127.0.0.1,127.0.0.1:8000,::1
IS_ADMIN_VUEFILEMANAGER_BAR=true

View File

@@ -51,4 +51,9 @@ return [
'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
'redirect' => env('APP_URL') . '/socialite/facebook/callback',
],
'recaptcha' => [
'client_id' => env('RECAPTCHA_CLIENT_ID'),
'client_secret' => env('RECAPTCHA_CLIENT_SECRET'),
],
];

19
package-lock.json generated
View File

@@ -9103,6 +9103,11 @@
"readable-stream": "^2.0.2"
}
},
"recaptcha-v3": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/recaptcha-v3/-/recaptcha-v3-1.10.0.tgz",
"integrity": "sha512-aGTxYSk3FFNKnXeKDbLpgRDRyIHRZNBF5HyaXXAN1Aj4TSyyZvmoAn9CylvpqLV3pYpIQavwc+2rzhNFn5SsLQ=="
},
"recast": {
"version": "0.11.23",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz",
@@ -11613,9 +11618,9 @@
"dev": true
},
"vue-i18n": {
"version": "8.26.8",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.26.8.tgz",
"integrity": "sha512-BN2OXolO15AKS95yNF8oOtARibaO6RxyKkAYNV4XpOmL7S4eVZYMIDtyvDv+XGZaiUmBJSH9mdNqzexvGMnK2A=="
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.27.0.tgz",
"integrity": "sha512-SX35iJHL5PJ4Gfh0Mo/q0shyHiI2V6Zkh51c+k8E9O1RKv5BQyYrCxRzpvPrsIOJEnLaeiovet3dsUB0e/kDzw=="
},
"vue-loader": {
"version": "15.9.7",
@@ -11638,6 +11643,14 @@
"vue": "^2.6.7"
}
},
"vue-recaptcha-v3": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/vue-recaptcha-v3/-/vue-recaptcha-v3-1.9.0.tgz",
"integrity": "sha512-WQIlhcOcETk3SYbEC88podUSq1J7UjmHpKgJhSy0Xm3DAjTIPjl19g9kn5KRdyTxNLiS/eR5C6H3Jk3c7b9baA==",
"requires": {
"recaptcha-v3": "^1.8.0"
}
},
"vue-resize-sensor": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",

View File

@@ -31,8 +31,9 @@
"vee-validate": "^3.4.14",
"vue": "^2.6.14",
"vue-feather-icons": "^5.1.0",
"vue-i18n": "^8.26.8",
"vue-i18n": "^8.27.0",
"vue-paystack": "^2.0.4",
"vue-recaptcha-v3": "^1.9.0",
"vue-router": "^3.5.3",
"vuex": "^3.6.2"
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -26,6 +26,15 @@ const ValidatorHelpers = {
Vue.prototype.$isInvalidEmail = function (email) {
return email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) === null
}
Vue.prototype.$reCaptchaToken = async function (action) {
await this.$recaptchaLoaded()
let token = await this.$recaptcha(action)
return token
}
}
}

View File

@@ -11,7 +11,8 @@ import SubscriptionHelpers from "./helpers/SubscriptionHelpers";
import ValidatorHelpers from "./helpers/ValidatorHelpers";
import functionHelpers from "./helpers/functionHelpers";
import AlertHelpers from "./helpers/AlertHelpers";
import itemHelpers from "./helpers/itemHelpers"
import itemHelpers from "./helpers/itemHelpers";
import { VueReCaptcha } from 'vue-recaptcha-v3';
Vue.use(VueRouter);
Vue.use(SubscriptionHelpers);
@@ -19,6 +20,11 @@ Vue.use(ValidatorHelpers);
Vue.use(functionHelpers);
Vue.use(AlertHelpers);
Vue.use(itemHelpers);
Vue.use(VueReCaptcha, { siteKey: config.recaptcha_client_id,
loaderOptions: {
autoHideBadge: true
}
})
Vue.config.productionTip = false;

View File

@@ -141,6 +141,11 @@ const mutations = {
state.config.allowedGithubLogin = true
state.config.isGithubLoginConfigured = true
}
if (service === 'recaptcha') {
state.config.allowedRecaptcha = true
state.config.isRecaptchaConfigured = true
}
},
SET_STRIPE_CREDENTIALS(state, data) {
state.config.stripe_public_key = data.key

View File

@@ -85,6 +85,56 @@
</AppInputSwitch>
</div>
<!-- ReCaptcha -->
<div class="card shadow-card">
<img src="/assets/others/recaptcha.svg" alt="reCaptcha" class="mb-8 h-10">
<AppInputSwitch :title="$t('Allow ReCaptcha')" :description="$t('ReCaptcha will be allowed on Registration and Contact Us forms.')" :is-last="! recaptcha.allowedService">
<SwitchInput
@input="$updateText('/admin/settings', 'allowed_recaptcha', recaptcha.allowedService)"
v-model="recaptcha.allowedService"
class="switch"
:state="recaptcha.allowedService"
/>
</AppInputSwitch>
<div v-if="config.isRecaptchaConfigured && recaptcha.allowedService" @click="recaptcha.isVisibleCredentialsForm = !recaptcha.isVisibleCredentialsForm" class="flex items-center cursor-pointer" :class="{'mb-4': recaptcha.isVisibleCredentialsForm}">
<edit2-icon size="12" class="vue-feather text-theme mr-2" />
<b class="text-xs">{{ $t('Update Your Credentials') }}</b>
</div>
<!--Set up recaptcha credentials-->
<ValidationObserver
v-if="(! config.isRecaptchaConfigured || recaptcha.isVisibleCredentialsForm) && recaptcha.allowedService"
@submit.prevent="storeCredentials('recaptcha')"
ref="credentialsForm"
v-slot="{ invalid }"
tag="form"
class="p-5 shadow-lg rounded-xl"
>
<FormLabel v-if="! config.isRecaptchaConfigured" icon="shield">
{{ $t('Configure Credentials') }}
</FormLabel>
<ValidationProvider tag="div" mode="passive" name="Site Key" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('Site Key')" :error="errors[0]">
<input v-model="recaptcha.credentials.client_id" :placeholder="$t('Paste your Site Key here')" type="text" :class="{'border-red': errors[0]}" class="focus-border-theme input-dark" />
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Secret key" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('Secret Key')" :error="errors[0]">
<input v-model="recaptcha.credentials.client_secret" :placeholder="$t('Paste your Secret key here')" type="text" :class="{'border-red': errors[0]}" class="focus-border-theme input-dark" />
</AppInputText>
</ValidationProvider>
<ButtonBase :disabled="isLoading" :loading="isLoading" button-style="theme" type="submit" class="w-full">
{{ $t('Store Credentials') }}
</ButtonBase>
</ValidationObserver>
</div>
<!--Facebook Social Authentication-->
<div class="card shadow-card">
<img :src="$getSocialLogo('facebook')" alt="Facebook" class="mb-8 h-5">
@@ -106,7 +156,7 @@
<!--Set up facebook credentials-->
<ValidationObserver
v-if="(! config.isFacebookLoginConfigured || facebook.isVisibleCredentialsForm) && facebook.allowedService"
@submit.prevent="storeCredentials('facebook')"
@submit.prevent="storeCredentials('facebook_login')"
ref="credentialsForm"
v-slot="{ invalid }"
tag="form"
@@ -156,7 +206,7 @@
<!--Set up Google credentials-->
<ValidationObserver
v-if="(! config.isGoogleLoginConfigured || google.isVisibleCredentialsForm) && google.allowedService"
@submit.prevent="storeCredentials('google')"
@submit.prevent="storeCredentials('google_login')"
ref="credentialsForm"
v-slot="{ invalid }"
tag="form"
@@ -206,7 +256,7 @@
<!--Set up github credentials-->
<ValidationObserver
v-if="(! config.isGithubLoginConfigured || github.isVisibleCredentialsForm) && github.allowedService"
@submit.prevent="storeCredentials('github')"
@submit.prevent="storeCredentials('github_login')"
ref="credentialsForm"
v-slot="{ invalid }"
tag="form"
@@ -289,6 +339,14 @@
isLoading: true,
isFlushingCache: false,
app: undefined,
recaptcha: {
allowedService: false,
isVisibleCredentialsForm: false,
credentials: {
key: undefined,
secret: undefined,
},
},
facebook: {
allowedService: false,
isVisibleCredentialsForm: false,
@@ -372,6 +430,7 @@
}
},
mounted() {
this.recaptcha.allowedService = this.config.allowedRecaptcha
this.facebook.allowedService = this.config.allowedFacebookLogin
this.google.allowedService = this.config.allowedGoogleLogin
this.github.allowedService = this.config.allowedGithubLogin

View File

@@ -122,6 +122,7 @@
email: '',
password: '',
password_confirmation: '',
reCaptcha:null,
},
}
},
@@ -136,6 +137,13 @@
// Start loading
this.isLoading = true
// Get ReCaptcha token
if(config.allowedRecaptcha) {
this.register.reCaptcha = await this.$reCaptchaToken('register').then((response) => {
return response
})
}
// Send request to get user token
axios
.post('/api/register', this.register)

View File

@@ -92,6 +92,7 @@
contact: {
email: '',
message: '',
reCaptcha: null,
},
}
},
@@ -106,6 +107,13 @@
// Start loading
this.isLoading = true
// Get ReCaptcha token
if(config.allowedRecaptcha) {
this.register.reCaptcha = await this.$reCaptchaToken('register').then((response) => {
return response
})
}
// Send request to get user token
axios
.post('/api/contact', this.contact)

View File

@@ -128,6 +128,11 @@
stripe_public_key: '{{ env('STRIPE_PUBLIC_KEY') }}',
stripe_payment_description: '{{ $settings->stripe_payment_description ?? '' }}',
// ReCaptcha
recaptcha_client_id: '{{ env('RECAPTCHA_CLIENT_ID') }}',
allowedRecaptcha: {{ $settings->allowed_recaptcha ?? 0 }},
isRecaptchaConfigured: {{ env('RECAPTCHA_CLIENT_ID') ? 1 : 0 }},
// Social logins
allowedFacebookLogin: {{ $settings->allowed_facebook_login ?? 0 }},
isFacebookLoginConfigured: {{ env('FACEBOOK_CLIENT_ID') ? 1 : 0 }},

View File

@@ -2,8 +2,10 @@
namespace App\Users\Requests;
use App\Users\Rules\EmailProvider;
use App\Users\Rules\ReCaptchaRules;
use Illuminate\Foundation\Http\FormRequest;
use App\Users\Rules\PasswordValidationRules;
use Illuminate\Validation\Rules\RequiredIf;
class RegisterUserRequest extends FormRequest
{
@@ -30,6 +32,7 @@ class RegisterUserRequest extends FormRequest
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email', new EmailProvider],
'name' => 'required|string|max:255',
'password' => $this->passwordRules(),
'reCaptcha' => [new RequiredIf(get_settings('allowed_recaptcha') == 1), 'string', app(ReCaptchaRules::class)]
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Users\Rules;
use GuzzleHttp\Client;
use Illuminate\Contracts\Validation\Rule;
class ReCaptchaRules implements Rule
{
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
$client = new Client();
$response = $client->post('https://www.google.com/recaptcha/api/siteverify',
[
'form_params' => [
'secret' => env('RECAPTCHA_CLIENT_SECRET', false),
'remoteip' => request()->getClientIp(),
'response' => $value
]
]
);
$body = json_decode((string)$response->getBody());
return $body->success;
}
public function message(): string
{
return 'Are you a robot?';
}
}

View File

@@ -1,7 +1,9 @@
<?php
namespace Domain\Homepage\Requests;
use App\Users\Rules\ReCaptchaRules;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\RequiredIf;
class SendContactMessageRequest extends FormRequest
{
@@ -25,6 +27,7 @@ class SendContactMessageRequest extends FormRequest
return [
'email' => 'required|email',
'message' => 'required|string',
'reCaptcha' => [new RequiredIf(get_settings('allowed_recaptcha') == 1), 'string', app(ReCaptchaRules::class)]
];
}
}

View File

@@ -18,7 +18,7 @@ class StoreSocialServiceCredentialsController
// Set on social login
Setting::updateOrCreate([
'name' => "allowed_{$request->input('service')}_login",
'name' => "allowed_{$request->input('service')}",
], [
'value' => 1,
]);
@@ -27,7 +27,7 @@ class StoreSocialServiceCredentialsController
if (! app()->runningUnitTests()) {
$credentials = [
'facebook' => [
'FACEBOOK_CLIENT_ID' => $request->input('client_id'),
'FACEBOOK_CLIENT_ID' => $request->input('client_id'),
'FACEBOOK_CLIENT_SECRET' => $request->input('client_secret'),
],
'google' => [
@@ -38,6 +38,11 @@ class StoreSocialServiceCredentialsController
'GITHUB_CLIENT_ID' => $request->input('client_id'),
'GITHUB_CLIENT_SECRET' => $request->input('client_secret'),
],
'recaptcha' => [
'RECAPTCHA_CLIENT_ID' => $request->input('client_id'),
'RECAPTCHA_CLIENT_SECRET' => $request->input('client_secret'),
],
];
// Store credentials into the .env file

View File

@@ -5,6 +5,7 @@ use Storage;
use Notification;
use Tests\TestCase;
use App\Users\Models\User;
use App\Users\Rules\ReCaptchaRules;
use Domain\Settings\Models\Setting;
use Illuminate\Support\Facades\Password;
use App\Users\Notifications\ResetPassword;
@@ -329,4 +330,33 @@ class SignFlowTest extends TestCase
'email' => $user->email,
]);
}
/**
* @test
*/
public function it_create_user_from_register_form_with_reCaptcha()
{
Setting::updateOrCreate([
'name' => 'allowed_recaptcha',
], [
'value' => 1,
]);
$this->mock(ReCaptchaRules::class, function ($mock) {
$mock->shouldReceive('passes')->andReturn(true);
});
$this->postJson('api/register', [
'email' => 'john@doe.com',
'password' => 'SecretPassword',
'password_confirmation' => 'SecretPassword',
'name' => 'John Doe',
'reCaptcha' => 'fakeToken'
])->assertStatus(201);
$this
->assertDatabaseHas('users', [
'email' => 'john@doe.com',
]);
}
}