login hcaptcha

This commit is contained in:
elvira
2026-09-25 16:19:56 +03:00
parent d43b615297
commit 3347febb3c
7 changed files with 118 additions and 16 deletions
+3
View File
@@ -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']));
+66
View File
@@ -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
View File
@@ -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",
+9
View File
@@ -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'),
+17
View File
@@ -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>
+1 -1
View File
@@ -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'],