generated from boboko/starter
login hcaptcha
This commit is contained in:
@@ -61,6 +61,9 @@ MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
CONTACT_EMAIL=
|
||||
|
||||
HCAPTCHA_SITEKEY=
|
||||
HCAPTCHA_SECRET=
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Rules\HCaptcha;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -43,8 +44,13 @@ public function create(string $locale, Request $request): View
|
||||
|
||||
public function send(string $locale, Request $request, UserOtpService $otp): RedirectResponse
|
||||
{
|
||||
// Captcha only here: verify() and resend() need the email this step
|
||||
// puts in the session, so they can't be reached without passing it.
|
||||
$validated = $request->validate([
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'h-captcha-response' => ['bail', 'required', new HCaptcha],
|
||||
], [
|
||||
'h-captcha-response.required' => __('storefront.auth.captcha_failed'),
|
||||
]);
|
||||
|
||||
$email = Str::lower(trim($validated['email']));
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Verifies the `h-captcha-response` token the hCaptcha widget adds to a form.
|
||||
* Use it as `'h-captcha-response' => ['required', new HCaptcha]`.
|
||||
*
|
||||
* Fails closed: if hCaptcha can't be reached the submission is rejected, since
|
||||
* letting it through would reopen the hole this exists to close (bots making
|
||||
* us send email to arbitrary addresses).
|
||||
*/
|
||||
class HCaptcha implements ValidationRule
|
||||
{
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (! is_string($value) || $value === '') {
|
||||
$fail(__('storefront.auth.captcha_failed'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::asForm()
|
||||
->timeout(5)
|
||||
->post('https://api.hcaptcha.com/siteverify', [
|
||||
'secret' => config('services.hcaptcha.secret'),
|
||||
'response' => $value,
|
||||
// Rejects tokens solved against someone else's sitekey.
|
||||
'sitekey' => config('services.hcaptcha.sitekey'),
|
||||
'remoteip' => request()->ip(),
|
||||
]);
|
||||
} catch (ConnectionException $e) {
|
||||
Log::warning('hCaptcha siteverify unreachable', ['error' => $e->getMessage()]);
|
||||
$fail(__('storefront.auth.captcha_failed'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $response->successful() || $response->json('success') !== true) {
|
||||
// A bad/missing secret or sitekey would otherwise look like every
|
||||
// shopper failing the captcha.
|
||||
$configErrors = array_intersect((array) $response->json('error-codes'), [
|
||||
'missing-input-secret',
|
||||
'invalid-input-secret',
|
||||
'sitekey-secret-mismatch',
|
||||
'invalid-sitekey',
|
||||
]);
|
||||
|
||||
if ($response->failed() || $configErrors) {
|
||||
Log::warning('hCaptcha siteverify error', [
|
||||
'status' => $response->status(),
|
||||
'error-codes' => $response->json('error-codes'),
|
||||
]);
|
||||
}
|
||||
|
||||
$fail(__('storefront.auth.captcha_failed'));
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+16
-15
@@ -515,11 +515,11 @@
|
||||
},
|
||||
{
|
||||
"name": "boboko/core",
|
||||
"version": "0.20.0",
|
||||
"version": "0.22.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://code.radical-elements.com/boboko/core.git",
|
||||
"reference": "c7035d678275a6c7aae6eb2a2ee1b12569da1a3d"
|
||||
"reference": "985f53efa2d34849f35c8e0f4129f861be762e76"
|
||||
},
|
||||
"require": {
|
||||
"laravel/framework": "^12.0",
|
||||
@@ -557,6 +557,7 @@
|
||||
"Modules\\Core\\Providers\\CatalogServiceProvider",
|
||||
"Modules\\Core\\Providers\\CartServiceProvider",
|
||||
"Modules\\Core\\Providers\\ReviewServiceProvider",
|
||||
"Modules\\Core\\Providers\\FileServiceProvider",
|
||||
"Modules\\Core\\Providers\\ShippingServiceProvider",
|
||||
"Modules\\Core\\Providers\\OrderServiceProvider",
|
||||
"Modules\\Core\\Providers\\PrivacyServiceProvider"
|
||||
@@ -569,7 +570,7 @@
|
||||
}
|
||||
},
|
||||
"description": "Core module — authentication and shared panel behaviour",
|
||||
"time": "2026-09-23T06:47:28+00:00"
|
||||
"time": "2026-09-25T12:58:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -2937,16 +2938,16 @@
|
||||
},
|
||||
{
|
||||
"name": "kirschbaum-development/eloquent-power-joins",
|
||||
"version": "4.3.3",
|
||||
"version": "4.3.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kirschbaum-development/eloquent-power-joins.git",
|
||||
"reference": "c609dbbe4ad2051b667e937f1ab554067519d64b"
|
||||
"reference": "a0e6c0420c381861e1830693c4b74bdac7f7fb5f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/c609dbbe4ad2051b667e937f1ab554067519d64b",
|
||||
"reference": "c609dbbe4ad2051b667e937f1ab554067519d64b",
|
||||
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/a0e6c0420c381861e1830693c4b74bdac7f7fb5f",
|
||||
"reference": "a0e6c0420c381861e1830693c4b74bdac7f7fb5f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2994,9 +2995,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues",
|
||||
"source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.3.3"
|
||||
"source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.3.4"
|
||||
},
|
||||
"time": "2026-07-23T11:41:37+00:00"
|
||||
"time": "2026-09-25T11:10:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
@@ -12600,16 +12601,16 @@
|
||||
},
|
||||
{
|
||||
"name": "technikermathe/blade-lucide-icons",
|
||||
"version": "v3.181.0",
|
||||
"version": "v3.182.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PascaleBeier/blade-lucide-icons.git",
|
||||
"reference": "64ccac4ecfe1b833e9a5cf1ebe7216acbe3cac98"
|
||||
"reference": "11253aa7d9aa7430da61d3534c62dc69a95b703b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PascaleBeier/blade-lucide-icons/zipball/64ccac4ecfe1b833e9a5cf1ebe7216acbe3cac98",
|
||||
"reference": "64ccac4ecfe1b833e9a5cf1ebe7216acbe3cac98",
|
||||
"url": "https://api.github.com/repos/PascaleBeier/blade-lucide-icons/zipball/11253aa7d9aa7430da61d3534c62dc69a95b703b",
|
||||
"reference": "11253aa7d9aa7430da61d3534c62dc69a95b703b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12659,9 +12660,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PascaleBeier/blade-lucide-icons/issues",
|
||||
"source": "https://github.com/PascaleBeier/blade-lucide-icons/tree/v3.181.0"
|
||||
"source": "https://github.com/PascaleBeier/blade-lucide-icons/tree/v3.182.0"
|
||||
},
|
||||
"time": "2026-09-18T02:09:04+00:00"
|
||||
"time": "2026-09-25T02:29:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tijsverkoyen/css-to-inline-styles",
|
||||
|
||||
@@ -40,6 +40,15 @@
|
||||
'email' => env('CONTACT_EMAIL'),
|
||||
],
|
||||
|
||||
// Bot check on guest forms (login), verified by App\Rules\HCaptcha. For
|
||||
// local dev use hCaptcha's test keys, the real ones reject localhost:
|
||||
// sitekey 10000000-ffff-ffff-ffff-000000000001,
|
||||
// secret 0x0000000000000000000000000000000000000000.
|
||||
'hcaptcha' => [
|
||||
'sitekey' => env('HCAPTCHA_SITEKEY'),
|
||||
'secret' => env('HCAPTCHA_SECRET'),
|
||||
],
|
||||
|
||||
'stoic' => [
|
||||
'sso_secret' => env('STOIC_SSO_SECRET'),
|
||||
'host' => env('STOIC_HOST'),
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
<meta name="robots" content="noindex, follow">
|
||||
@endpush
|
||||
|
||||
{{-- Only on pages with a captcha, not in the global layout. --}}
|
||||
@push('scripts')
|
||||
<script src="https://js.hcaptcha.com/1/api.js?hl={{ app()->getLocale() }}" async defer></script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<section class="mx-auto max-w-2xl px-4 py-20 sm:px-8 sm:py-32">
|
||||
@@ -52,6 +57,18 @@
|
||||
]) !!}
|
||||
</p>
|
||||
|
||||
{{-- hCaptcha checkbox, verified by App\Rules\HCaptcha. The widget adds
|
||||
the `h-captcha-response` field itself. Fixed height reserves the
|
||||
iframe's space so the button doesn't jump when it loads. --}}
|
||||
<x-ui.field for="login-captcha" :error="$errors->first('h-captcha-response')">
|
||||
<div
|
||||
id="login-captcha"
|
||||
class="h-captcha min-h-[78px]"
|
||||
data-sitekey="{{ config('services.hcaptcha.sitekey') }}"
|
||||
@if ($errors->has('h-captcha-response')) aria-describedby="login-captcha-error" @endif
|
||||
></div>
|
||||
</x-ui.field>
|
||||
|
||||
<div>
|
||||
<x-ui.button type="submit">{{ __('storefront.auth.send_code') }}</x-ui.button>
|
||||
</div>
|
||||
|
||||
@@ -270,7 +270,7 @@ class="flex flex-col gap-6"
|
||||
@if(!empty($product['recommendations']))
|
||||
<x-product-grid
|
||||
class="mt-20"
|
||||
title="Σχετικά προϊόντα"
|
||||
:title="__('storefront.product.related')"
|
||||
:products="collect($product['recommendations'])->map(fn (array $recommendation) => [
|
||||
'name' => $recommendation['name'],
|
||||
'price' => $recommendation['price'],
|
||||
|
||||
Reference in New Issue
Block a user