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:
baodev0
2026-10-12 15:53:43 +07:00
co-authored by Claude Opus 5.5
parent 823af4a56a
commit e888a6ed5d
33 changed files with 1022 additions and 52 deletions
+10 -9
View File
@@ -47,7 +47,8 @@ class ReencryptSecrets extends Command
$failed += $this->reencryptColumn($table, $column, fn (string $value) => Crypt::encryptString(Crypt::decryptString($value)));
}
$failed += $this->reencryptSmtpSetting();
$failed += $this->reencryptJsonSetting('smtp', 'password');
$failed += $this->reencryptJsonSetting('turnstile', 'secret_key');
return $failed === 0 ? self::SUCCESS : self::FAILURE;
}
@@ -89,27 +90,27 @@ class ReencryptSecrets extends Command
return $failed;
}
private function reencryptSmtpSetting(): int
private function reencryptJsonSetting(string $key, string $field): int
{
$setting = DB::table('setting')->where('key', 'smtp')->first();
$smtp = $setting ? json_decode((string) $setting->value, true) : null;
$setting = DB::table('setting')->where('key', $key)->first();
$value = $setting ? json_decode((string) $setting->value, true) : null;
if (empty($smtp['password'])) {
if (empty($value[$field])) {
return 0;
}
try {
$smtp['password'] = SignRSA::EncryptFunc($this->decryptRsa($smtp['password']));
$value[$field] = SignRSA::EncryptFunc($this->decryptRsa($value[$field]));
} catch (Throwable) {
$this->warn('setting[smtp].password: cannot decrypt with current or previous key');
$this->warn("setting[{$key}].{$field}: cannot decrypt with current or previous key");
return 1;
}
if (!$this->option('dry-run')) {
DB::table('setting')->where('key', 'smtp')->update(['value' => json_encode($smtp)]);
DB::table('setting')->where('key', $key)->update(['value' => json_encode($value)]);
}
$this->info('setting[smtp].password: re-encrypted');
$this->info("setting[{$key}].{$field}: re-encrypted");
return 0;
}