新增腾讯天御验证码认证门禁

This commit is contained in:
lyen1688
2026-08-04 15:09:29 +08:00
parent 825ca7b1fc
commit e592c5f9e0
86 changed files with 3683 additions and 262 deletions
+180 -1
View File
@@ -1990,7 +1990,11 @@
{{ t("admin.settings.turnstile.enableTurnstileHint") }}
</p>
</div>
<Toggle v-model="form.turnstile_enabled" />
<Toggle
v-model="form.turnstile_enabled"
data-testid="turnstile-enabled-toggle"
@update:model-value="onTurnstileToggle"
/>
</div>
<!-- Turnstile Keys - Only show when enabled -->
@@ -2050,6 +2054,151 @@
</div>
</div>
<!-- 腾讯天御验证码设置 -->
<div class="card">
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t("admin.settings.tencentCaptcha.title") }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.tencentCaptcha.description") }}
</p>
</div>
<div class="space-y-5 p-6">
<div class="flex items-center justify-between gap-6">
<div>
<label class="font-medium text-gray-900 dark:text-white">
{{ t("admin.settings.tencentCaptcha.enable") }}
</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.tencentCaptcha.enableHint") }}
</p>
</div>
<Toggle
v-model="form.tencent_captcha_enabled"
data-testid="tencent-captcha-enabled-toggle"
@update:model-value="onTencentCaptchaToggle"
/>
</div>
<div
v-if="form.tencent_captcha_enabled"
class="border-t border-gray-100 pt-4 dark:border-dark-700"
>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="md:col-span-2">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{{ t("admin.settings.tencentCaptcha.appCredentialsTitle") }}
</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.tencentCaptcha.appCredentialsHint") }}
</p>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("admin.settings.tencentCaptcha.appId") }}
</label>
<input
v-model="form.tencent_captcha_app_id"
type="text"
inputmode="numeric"
class="input font-mono text-sm"
placeholder="123456789"
/>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("admin.settings.tencentCaptcha.appSecretKey") }}
</label>
<input
v-model="form.tencent_captcha_app_secret_key"
type="password"
autocomplete="new-password"
class="input font-mono text-sm"
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ form.tencent_captcha_app_secret_key_configured ? t("admin.settings.tencentCaptcha.configured") : t("admin.settings.tencentCaptcha.required") }}
</p>
</div>
<div class="border-t border-gray-100 pt-5 md:col-span-2 dark:border-dark-700">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{{ t("admin.settings.tencentCaptcha.cloudCredentialsTitle") }}
</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.tencentCaptcha.cloudCredentialsHint") }}
</p>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("admin.settings.tencentCaptcha.cloudSecretId") }}
</label>
<input
v-model="form.tencent_captcha_cloud_secret_id"
type="password"
autocomplete="new-password"
class="input font-mono text-sm"
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ form.tencent_captcha_cloud_secret_id_configured ? t("admin.settings.tencentCaptcha.configured") : t("admin.settings.tencentCaptcha.required") }}
</p>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t("admin.settings.tencentCaptcha.cloudSecretKey") }}
</label>
<input
v-model="form.tencent_captcha_cloud_secret_key"
type="password"
autocomplete="new-password"
class="input font-mono text-sm"
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ form.tencent_captcha_cloud_secret_key_configured ? t("admin.settings.tencentCaptcha.configured") : t("admin.settings.tencentCaptcha.required") }}
</p>
</div>
</div>
<p class="mt-5 text-xs text-amber-600 dark:text-amber-400">
{{ t("admin.settings.tencentCaptcha.mutualExclusion") }}
</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.tencentCaptcha.camPermissionHint") }}
</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.tencentCaptcha.aidEncryptedHint") }}
</p>
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-sm">
<a
href="https://console.cloud.tencent.com/captcha"
target="_blank"
rel="noopener noreferrer"
class="text-primary-600 hover:text-primary-500"
>
{{ t("admin.settings.tencentCaptcha.openCaptchaConsole") }}
</a>
<a
href="https://console.cloud.tencent.com/cam/capi"
target="_blank"
rel="noopener noreferrer"
class="text-primary-600 hover:text-primary-500"
>
{{ t("admin.settings.tencentCaptcha.createCloudKeys") }}
</a>
<a
href="https://cloud.tencent.com/document/product/1110/36841"
target="_blank"
rel="noopener noreferrer"
class="text-primary-600 hover:text-primary-500"
>
{{ t("admin.settings.tencentCaptcha.openWebDocs") }}
</a>
</div>
</div>
</div>
</div>
<!-- LinuxDo Connect OAuth 登录 -->
<div class="card">
<div
@@ -8779,6 +8928,9 @@ type SettingsForm = Omit<
> & {
smtp_password: string;
turnstile_secret_key: string;
tencent_captcha_app_secret_key: string;
tencent_captcha_cloud_secret_id: string;
tencent_captcha_cloud_secret_key: string;
linuxdo_connect_client_secret: string;
dingtalk_connect_client_secret: string;
wechat_connect_app_secret: string;
@@ -8908,6 +9060,14 @@ const form = reactive<SettingsForm>({
turnstile_site_key: "",
turnstile_secret_key: "",
turnstile_secret_key_configured: false,
tencent_captcha_enabled: false,
tencent_captcha_app_id: "",
tencent_captcha_app_secret_key: "",
tencent_captcha_app_secret_key_configured: false,
tencent_captcha_cloud_secret_id: "",
tencent_captcha_cloud_secret_id_configured: false,
tencent_captcha_cloud_secret_key: "",
tencent_captcha_cloud_secret_key_configured: false,
api_key_acl_trust_forwarded_ip: true,
forwarded_client_ip_headers: [],
// LinuxDo Connect OAuth 登录
@@ -9067,6 +9227,14 @@ const form = reactive<SettingsForm>({
allow_user_view_error_requests: false,
});
function onTurnstileToggle(enabled: boolean): void {
if (enabled) form.tencent_captcha_enabled = false;
}
function onTencentCaptchaToggle(enabled: boolean): void {
if (enabled) form.turnstile_enabled = false;
}
type OpenAIAdvancedSchedulerOverrideKey =
| "openai_advanced_scheduler_lb_top_k"
| "openai_advanced_scheduler_weight_priority"
@@ -10024,6 +10192,9 @@ async function loadSettings() {
form.smtp_password = "";
smtpPasswordManuallyEdited.value = false;
form.turnstile_secret_key = "";
form.tencent_captcha_app_secret_key = "";
form.tencent_captcha_cloud_secret_id = "";
form.tencent_captcha_cloud_secret_key = "";
form.linuxdo_connect_client_secret = "";
form.dingtalk_connect_client_secret = "";
form.github_oauth_client_secret = "";
@@ -10393,6 +10564,14 @@ async function saveSettings() {
turnstile_enabled: form.turnstile_enabled,
turnstile_site_key: form.turnstile_site_key,
turnstile_secret_key: form.turnstile_secret_key || undefined,
tencent_captcha_enabled: form.tencent_captcha_enabled,
tencent_captcha_app_id: form.tencent_captcha_app_id,
tencent_captcha_app_secret_key:
form.tencent_captcha_app_secret_key || undefined,
tencent_captcha_cloud_secret_id:
form.tencent_captcha_cloud_secret_id || undefined,
tencent_captcha_cloud_secret_key:
form.tencent_captcha_cloud_secret_key || undefined,
api_key_acl_trust_forwarded_ip: form.api_key_acl_trust_forwarded_ip,
forwarded_client_ip_headers: form.forwarded_client_ip_headers,
linuxdo_connect_enabled: form.linuxdo_connect_enabled,
@@ -396,6 +396,11 @@ const baseSettingsResponse = {
turnstile_enabled: false,
turnstile_site_key: "",
turnstile_secret_key_configured: false,
tencent_captcha_enabled: false,
tencent_captcha_app_id: "",
tencent_captcha_app_secret_key_configured: false,
tencent_captcha_cloud_secret_id_configured: false,
tencent_captcha_cloud_secret_key_configured: false,
api_key_acl_trust_forwarded_ip: true,
forwarded_client_ip_headers: [],
linuxdo_connect_enabled: false,
@@ -767,6 +772,50 @@ describe("admin SettingsView payment visible method controls", () => {
);
});
it("腾讯天御验证码与 Turnstile 开关互斥并保存四项配置", async () => {
const wrapper = mountView();
await flushPromises();
await openSecurityTab(wrapper);
const turnstileToggle = wrapper.get('[data-testid="turnstile-enabled-toggle"]');
const tencentToggle = wrapper.get('[data-testid="tencent-captcha-enabled-toggle"]');
await turnstileToggle.setValue(true);
expect((turnstileToggle.element as HTMLInputElement).checked).toBe(true);
await tencentToggle.setValue(true);
expect((turnstileToggle.element as HTMLInputElement).checked).toBe(false);
expect((tencentToggle.element as HTMLInputElement).checked).toBe(true);
const card = wrapper
.findAll(".card")
.find((node) => node.text().includes("admin.settings.tencentCaptcha.title"));
expect(card).toBeDefined();
expect(card!.get('a[href="https://console.cloud.tencent.com/captcha"]').exists()).toBe(true);
expect(card!.get('a[href="https://console.cloud.tencent.com/cam/capi"]').exists()).toBe(true);
expect(
card!.get('a[href="https://cloud.tencent.com/document/product/1110/36841"]').exists(),
).toBe(true);
const inputs = card!.findAll("input").filter((input) => input.attributes("type") !== "checkbox");
await inputs[0]!.setValue("123456789");
await inputs[1]!.setValue("app-secret-value");
await inputs[2]!.setValue("cloud-secret-id-value");
await inputs[3]!.setValue("cloud-secret-key-value");
await wrapper.find("form").trigger("submit.prevent");
await flushPromises();
expect(updateSettings).toHaveBeenCalledWith(
expect.objectContaining({
turnstile_enabled: false,
tencent_captcha_enabled: true,
tencent_captcha_app_id: "123456789",
tencent_captcha_app_secret_key: "app-secret-value",
tencent_captcha_cloud_secret_id: "cloud-secret-id-value",
tencent_captcha_cloud_secret_key: "cloud-secret-key-value",
}),
);
});
it("disables passkey sign-in when the RP configuration is unavailable", async () => {
getSettings.mockResolvedValueOnce({
...baseSettingsResponse,