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}" MAIL_FROM_NAME="${APP_NAME}"
CONTACT_EMAIL= CONTACT_EMAIL=
HCAPTCHA_SITEKEY=
HCAPTCHA_SECRET=
AWS_ACCESS_KEY_ID= AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1 AWS_DEFAULT_REGION=us-east-1
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Rules\HCaptcha;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; 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 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([ $validated = $request->validate([
'email' => ['required', 'email', 'max:255'], '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'])); $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", "name": "boboko/core",
"version": "0.20.0", "version": "0.22.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://code.radical-elements.com/boboko/core.git", "url": "https://code.radical-elements.com/boboko/core.git",
"reference": "c7035d678275a6c7aae6eb2a2ee1b12569da1a3d" "reference": "985f53efa2d34849f35c8e0f4129f861be762e76"
}, },
"require": { "require": {
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
@@ -557,6 +557,7 @@
"Modules\\Core\\Providers\\CatalogServiceProvider", "Modules\\Core\\Providers\\CatalogServiceProvider",
"Modules\\Core\\Providers\\CartServiceProvider", "Modules\\Core\\Providers\\CartServiceProvider",
"Modules\\Core\\Providers\\ReviewServiceProvider", "Modules\\Core\\Providers\\ReviewServiceProvider",
"Modules\\Core\\Providers\\FileServiceProvider",
"Modules\\Core\\Providers\\ShippingServiceProvider", "Modules\\Core\\Providers\\ShippingServiceProvider",
"Modules\\Core\\Providers\\OrderServiceProvider", "Modules\\Core\\Providers\\OrderServiceProvider",
"Modules\\Core\\Providers\\PrivacyServiceProvider" "Modules\\Core\\Providers\\PrivacyServiceProvider"
@@ -569,7 +570,7 @@
} }
}, },
"description": "Core module — authentication and shared panel behaviour", "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", "name": "brick/math",
@@ -2937,16 +2938,16 @@
}, },
{ {
"name": "kirschbaum-development/eloquent-power-joins", "name": "kirschbaum-development/eloquent-power-joins",
"version": "4.3.3", "version": "4.3.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/kirschbaum-development/eloquent-power-joins.git", "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git",
"reference": "c609dbbe4ad2051b667e937f1ab554067519d64b" "reference": "a0e6c0420c381861e1830693c4b74bdac7f7fb5f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/c609dbbe4ad2051b667e937f1ab554067519d64b", "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/a0e6c0420c381861e1830693c4b74bdac7f7fb5f",
"reference": "c609dbbe4ad2051b667e937f1ab554067519d64b", "reference": "a0e6c0420c381861e1830693c4b74bdac7f7fb5f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2994,9 +2995,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues", "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", "name": "laravel/framework",
@@ -12600,16 +12601,16 @@
}, },
{ {
"name": "technikermathe/blade-lucide-icons", "name": "technikermathe/blade-lucide-icons",
"version": "v3.181.0", "version": "v3.182.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PascaleBeier/blade-lucide-icons.git", "url": "https://github.com/PascaleBeier/blade-lucide-icons.git",
"reference": "64ccac4ecfe1b833e9a5cf1ebe7216acbe3cac98" "reference": "11253aa7d9aa7430da61d3534c62dc69a95b703b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PascaleBeier/blade-lucide-icons/zipball/64ccac4ecfe1b833e9a5cf1ebe7216acbe3cac98", "url": "https://api.github.com/repos/PascaleBeier/blade-lucide-icons/zipball/11253aa7d9aa7430da61d3534c62dc69a95b703b",
"reference": "64ccac4ecfe1b833e9a5cf1ebe7216acbe3cac98", "reference": "11253aa7d9aa7430da61d3534c62dc69a95b703b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -12659,9 +12660,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/PascaleBeier/blade-lucide-icons/issues", "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", "name": "tijsverkoyen/css-to-inline-styles",
+9
View File
@@ -40,6 +40,15 @@
'email' => env('CONTACT_EMAIL'), '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' => [ 'stoic' => [
'sso_secret' => env('STOIC_SSO_SECRET'), 'sso_secret' => env('STOIC_SSO_SECRET'),
'host' => env('STOIC_HOST'), 'host' => env('STOIC_HOST'),
+17
View File
@@ -6,6 +6,11 @@
<meta name="robots" content="noindex, follow"> <meta name="robots" content="noindex, follow">
@endpush @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('content')
<section class="mx-auto max-w-2xl px-4 py-20 sm:px-8 sm:py-32"> <section class="mx-auto max-w-2xl px-4 py-20 sm:px-8 sm:py-32">
@@ -52,6 +57,18 @@
]) !!} ]) !!}
</p> </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> <div>
<x-ui.button type="submit">{{ __('storefront.auth.send_code') }}</x-ui.button> <x-ui.button type="submit">{{ __('storefront.auth.send_code') }}</x-ui.button>
</div> </div>
+1 -1
View File
@@ -270,7 +270,7 @@ class="flex flex-col gap-6"
@if(!empty($product['recommendations'])) @if(!empty($product['recommendations']))
<x-product-grid <x-product-grid
class="mt-20" class="mt-20"
title="Σχετικά προϊόντα" :title="__('storefront.product.related')"
:products="collect($product['recommendations'])->map(fn (array $recommendation) => [ :products="collect($product['recommendations'])->map(fn (array $recommendation) => [
'name' => $recommendation['name'], 'name' => $recommendation['name'],
'price' => $recommendation['price'], 'price' => $recommendation['price'],