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}"
|
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']));
|
||||||
|
|||||||
@@ -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",
|
"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",
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
Reference in New Issue
Block a user