feat: Turnstile captcha, menu khách + nút tắt topbar cấu hình từ admin, ẩn setting nhạy cảm
- Turnstile (App\Support\Turnstile, middleware turnstile) cho login admin và login/register/forgot/reset của khách; cấu hình ở setting key=turnstile, secret mã hoá SignRSA. - Menu sidebar khách (setting client_menu) và nút tắt topbar (setting client_quick_links) do admin sửa qua administrator/client-menu[/quick-links], khách đọc v1/setting/menu, v1/setting/quick-links. - v1/setting và settings/public chỉ trả whitelist PublicSetting::FIELDS (trước đây lộ bot_token, inet_api_key). - Cài đặt tài khoản khách (preferences), rate limit theo IP cho register/resend-verify. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
823af4a56a
commit
e888a6ed5d
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Http\Facade\SignRSA;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use InvalidArgumentException;
|
||||
use Throwable;
|
||||
|
||||
final class Turnstile
|
||||
{
|
||||
public const SETTING_KEY = 'turnstile';
|
||||
|
||||
public const TOKEN_FIELD = 'cf-turnstile-response';
|
||||
|
||||
public const SECRET_MASK = '********';
|
||||
|
||||
private const VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
||||
|
||||
/**
|
||||
* @return array{enabled: bool, site_key: string, secret_key: string}
|
||||
*/
|
||||
public static function stored(): array
|
||||
{
|
||||
$value = json_decode((string) Setting::where('key', self::SETTING_KEY)->value('value'), true) ?: [];
|
||||
|
||||
return [
|
||||
'enabled' => (bool) ($value['enabled'] ?? false),
|
||||
'site_key' => (string) ($value['site_key'] ?? ''),
|
||||
'secret_key' => (string) ($value['secret_key'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
public static function siteKey(): ?string
|
||||
{
|
||||
$config = self::stored();
|
||||
|
||||
return $config['enabled'] && $config['site_key'] !== '' && $config['secret_key'] !== ''
|
||||
? $config['site_key']
|
||||
: null;
|
||||
}
|
||||
|
||||
public static function verify(?string $token, ?string $ip): bool
|
||||
{
|
||||
if (self::siteKey() === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($token === null || $token === '' || strlen($token) > 2048) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::asForm()->timeout(10)->post(self::VERIFY_URL, array_filter([
|
||||
'secret' => SignRSA::DecryptFunc(self::stored()['secret_key']),
|
||||
'response' => $token,
|
||||
'remoteip' => $ip,
|
||||
]));
|
||||
} catch (Throwable $e) {
|
||||
Log::warning('Turnstile siteverify failed', ['error' => $e->getMessage()]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($response->json('success') !== true) {
|
||||
Log::info('Turnstile rejected', ['codes' => $response->json('error-codes')]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public static function save(array $input): void
|
||||
{
|
||||
$current = self::stored();
|
||||
$secret = (string) ($input['secret_key'] ?? self::SECRET_MASK);
|
||||
|
||||
$config = [
|
||||
'enabled' => (bool) ($input['enabled'] ?? $current['enabled']),
|
||||
'site_key' => trim((string) ($input['site_key'] ?? $current['site_key'])),
|
||||
'secret_key' => match (true) {
|
||||
$secret === self::SECRET_MASK => $current['secret_key'],
|
||||
trim($secret) === '' => '',
|
||||
default => SignRSA::EncryptFunc(trim($secret)),
|
||||
},
|
||||
];
|
||||
|
||||
if ($config['enabled'] && ($config['site_key'] === '' || $config['secret_key'] === '')) {
|
||||
throw new InvalidArgumentException('Cần nhập Site key và Secret key trước khi bật Turnstile.');
|
||||
}
|
||||
|
||||
Setting::updateOrCreate(['key' => self::SETTING_KEY], ['value' => json_encode($config)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled: bool, site_key: string, secret_key: string}
|
||||
*/
|
||||
public static function forAdmin(): array
|
||||
{
|
||||
$config = self::stored();
|
||||
$config['secret_key'] = $config['secret_key'] === '' ? '' : self::SECRET_MASK;
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user