diff --git a/.env.example b/.env.example
index 75656fe6..5658fd5f 100644
--- a/.env.example
+++ b/.env.example
@@ -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
diff --git a/config/services.php b/config/services.php
index 184d6c81..4770d169 100644
--- a/config/services.php
+++ b/config/services.php
@@ -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'),
+ ],
];
diff --git a/package-lock.json b/package-lock.json
index fcf589f5..4cf54fae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index f301b354..9d127c57 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
diff --git a/public/assets/others/recaptcha.svg b/public/assets/others/recaptcha.svg
new file mode 100644
index 00000000..270413ee
--- /dev/null
+++ b/public/assets/others/recaptcha.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/resources/js/helpers/ValidatorHelpers.js b/resources/js/helpers/ValidatorHelpers.js
index 6494ded3..bd8f4003 100644
--- a/resources/js/helpers/ValidatorHelpers.js
+++ b/resources/js/helpers/ValidatorHelpers.js
@@ -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
+ }
}
}
diff --git a/resources/js/main.js b/resources/js/main.js
index ab0e359a..00ab7925 100644
--- a/resources/js/main.js
+++ b/resources/js/main.js
@@ -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;
diff --git a/resources/js/store/modules/app.js b/resources/js/store/modules/app.js
index 26656052..3af55f6b 100644
--- a/resources/js/store/modules/app.js
+++ b/resources/js/store/modules/app.js
@@ -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
diff --git a/resources/js/views/Admin/AppSettings/AppSettingsTabs/Others.vue b/resources/js/views/Admin/AppSettings/AppSettingsTabs/Others.vue
index 15235674..dbbae156 100644
--- a/resources/js/views/Admin/AppSettings/AppSettingsTabs/Others.vue
+++ b/resources/js/views/Admin/AppSettings/AppSettingsTabs/Others.vue
@@ -85,6 +85,56 @@
+
+
+

+
+
+
+
+
+
+
+ {{ $t('Update Your Credentials') }}
+
+
+
+
+
+ {{ $t('Configure Credentials') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('Store Credentials') }}
+
+
+
+
+
![Facebook]()
@@ -106,7 +156,7 @@
{
+ return response
+ })
+ }
+
// Send request to get user token
axios
.post('/api/register', this.register)
diff --git a/resources/js/views/Frontpage/ContactUs.vue b/resources/js/views/Frontpage/ContactUs.vue
index 47bebeb8..cdc5bf6a 100644
--- a/resources/js/views/Frontpage/ContactUs.vue
+++ b/resources/js/views/Frontpage/ContactUs.vue
@@ -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)
diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php
index 1f1d2c88..094331d1 100644
--- a/resources/views/index.blade.php
+++ b/resources/views/index.blade.php
@@ -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 }},
diff --git a/src/App/Users/Requests/RegisterUserRequest.php b/src/App/Users/Requests/RegisterUserRequest.php
index 8ed4e547..4514304b 100644
--- a/src/App/Users/Requests/RegisterUserRequest.php
+++ b/src/App/Users/Requests/RegisterUserRequest.php
@@ -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)]
];
}
}
diff --git a/src/App/Users/Rules/ReCaptchaRules.php b/src/App/Users/Rules/ReCaptchaRules.php
new file mode 100644
index 00000000..7fc419c3
--- /dev/null
+++ b/src/App/Users/Rules/ReCaptchaRules.php
@@ -0,0 +1,38 @@
+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?';
+ }
+}
\ No newline at end of file
diff --git a/src/Domain/Homepage/Requests/SendContactMessageRequest.php b/src/Domain/Homepage/Requests/SendContactMessageRequest.php
index db2835f8..8293b0e6 100644
--- a/src/Domain/Homepage/Requests/SendContactMessageRequest.php
+++ b/src/Domain/Homepage/Requests/SendContactMessageRequest.php
@@ -1,7 +1,9 @@
'required|email',
'message' => 'required|string',
+ 'reCaptcha' => [new RequiredIf(get_settings('allowed_recaptcha') == 1), 'string', app(ReCaptchaRules::class)]
];
}
}
diff --git a/src/Domain/Settings/Controllers/StoreSocialServiceCredentialsController.php b/src/Domain/Settings/Controllers/StoreSocialServiceCredentialsController.php
index f541a43d..37e6a0cc 100644
--- a/src/Domain/Settings/Controllers/StoreSocialServiceCredentialsController.php
+++ b/src/Domain/Settings/Controllers/StoreSocialServiceCredentialsController.php
@@ -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
diff --git a/tests/App/Users/SignFlowTest.php b/tests/App/Users/SignFlowTest.php
index 16e89eff..3203080b 100644
--- a/tests/App/Users/SignFlowTest.php
+++ b/tests/App/Users/SignFlowTest.php
@@ -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',
+ ]);
+ }
}