mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-03 00:33:13 +00:00
人机验证增加阿里云验证码 2.0
沿用腾讯天御验证码引入的多服务商模型:aliyun_captcha_enabled 作为独立 开关,与 Cloudflare Turnstile、腾讯天御三方互斥(保存校验 + 运行时 CAPTCHA_PROVIDER_CONFLICT)。后台「安全与认证」合并为单张人机验证卡片: 总开关 + 服务商单选(Turnstile / 腾讯天御 / 阿里云),选中即启用该家并 关闭其它,落库仍是三个独立开关键,由前端映射保证互斥。 阿里云侧同时支持 aliyun 中国站与国际站(alibabacloud.com):两站前端脚本、 region 取值与服务端 API 完全一致,仅账号与 AccessKey 相互独立,因此由 「服务地域」决定线路即可——中国内地走 captcha.cn-shanghai.aliyuncs.com, 非中国内地(新加坡)走 captcha.ap-southeast-1.aliyuncs.com,AccessKey 取自持有该实例的账号,无需在配置中区分站点。 - AliyunCaptchaService 对称 TencentCaptchaService:服务端校验走官方 SDK VerifyIntelligentCaptcha,调用异常按 fail-closed 拦截,与 Turnstile 网络错误行为对称;保存设置时真实探测 AK/SK 有效性 - 保护面对齐腾讯扩展入口:VerifyTencentCaptchaIfEnabled 通用化为 VerifyActionCaptchaIfEnabled,OAuth 登录启动、passkey 登录在阿里云 启用时同样拦截;Turnstile 维持既有覆盖不扩大 - 前端 AliyunCaptchaWidget 为表单内预验证按钮(popup 模式),同时暴露 verify() 供 OAuth 启动、passkey 等动作入口程序化弹窗;未预验证直接 提交时弹窗兜底。SDK 按钮绑定异步完成,弹窗未出现前按 tick 重试触发, 并轮询弹窗可见性识别用户关闭 - captchaVerifyParam 复用 turnstile_token 请求字段提交;公开设置下发 aliyun_captcha_enabled / scene_id / prefix / region - CSP 放行验证码 CDN:script-src/style-src 加 *.alicdn.com
This commit is contained in:
1 parent
d431c57f2e
commit
26e0a89323
57 files changed
+2445
-302
No files matched your search
@@ -1967,42 +1967,97 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare Turnstile Settings -->
|
||||
<!-- 人机验证 Settings -->
|
||||
<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.turnstile.title") }}
|
||||
{{ t("admin.settings.captcha.title") }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.turnstile.description") }}
|
||||
{{ t("admin.settings.captcha.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-5 p-6">
|
||||
<!-- Enable Turnstile -->
|
||||
<!-- Enable Captcha -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t("admin.settings.turnstile.enableTurnstile")
|
||||
t("admin.settings.captcha.enable")
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.turnstile.enableTurnstileHint") }}
|
||||
{{ t("admin.settings.captcha.enableHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
v-model="form.turnstile_enabled"
|
||||
data-testid="turnstile-enabled-toggle"
|
||||
@update:model-value="onTurnstileToggle"
|
||||
v-model="captchaMasterEnabled"
|
||||
data-testid="captcha-enabled-toggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Turnstile Keys - Only show when enabled -->
|
||||
<!-- Provider fields - Only show when enabled -->
|
||||
<div
|
||||
v-if="form.turnstile_enabled"
|
||||
v-if="captchaMasterEnabled"
|
||||
class="border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<!-- Provider Selector -->
|
||||
<div class="mb-6">
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.captcha.provider") }}
|
||||
</label>
|
||||
<div
|
||||
class="grid grid-cols-3 gap-2 rounded-lg bg-gray-100 p-1 dark:bg-dark-700"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="captcha-provider-turnstile"
|
||||
class="inline-flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition"
|
||||
:class="
|
||||
captchaProviderSelection === 'turnstile'
|
||||
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
|
||||
"
|
||||
@click="selectCaptchaProvider('turnstile')"
|
||||
>
|
||||
{{ t("admin.settings.captcha.providerTurnstile") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="captcha-provider-tencent"
|
||||
class="inline-flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition"
|
||||
:class="
|
||||
captchaProviderSelection === 'tencent'
|
||||
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
|
||||
"
|
||||
@click="selectCaptchaProvider('tencent')"
|
||||
>
|
||||
{{ t("admin.settings.captcha.providerTencent") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="captcha-provider-aliyun"
|
||||
class="inline-flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition"
|
||||
:class="
|
||||
captchaProviderSelection === 'aliyun'
|
||||
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
|
||||
"
|
||||
@click="selectCaptchaProvider('aliyun')"
|
||||
>
|
||||
{{ t("admin.settings.captcha.providerAliyun") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare Turnstile fields -->
|
||||
<div
|
||||
v-if="captchaProviderSelection === 'turnstile'"
|
||||
class="grid grid-cols-1 gap-6"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
@@ -2050,151 +2105,232 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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") }}
|
||||
<!-- Tencent Captcha fields -->
|
||||
<div v-else-if="captchaProviderSelection === 'tencent'">
|
||||
<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-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>
|
||||
<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>
|
||||
<!-- Aliyun Captcha 2.0 fields -->
|
||||
<div v-else class="grid grid-cols-1 gap-6">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.region") }}
|
||||
</label>
|
||||
<div
|
||||
class="grid grid-cols-2 gap-2 rounded-lg bg-gray-100 p-1 dark:bg-dark-700"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium transition"
|
||||
:class="
|
||||
form.aliyun_captcha_region !== 'sgp'
|
||||
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
|
||||
"
|
||||
@click="form.aliyun_captcha_region = 'cn'"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.regionCn") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium transition"
|
||||
:class="
|
||||
form.aliyun_captcha_region === 'sgp'
|
||||
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
|
||||
"
|
||||
@click="form.aliyun_captcha_region = 'sgp'"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.regionSgp") }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.aliyunCaptcha.regionHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.prefix") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.aliyun_captcha_prefix"
|
||||
type="text"
|
||||
class="input font-mono text-sm"
|
||||
placeholder="14xxxxx"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.aliyunCaptcha.prefixHint") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.tencentCaptcha.appId") }}
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.sceneId") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_app_id"
|
||||
v-model="form.aliyun_captcha_scene_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')"
|
||||
placeholder="1cxxxxxx"
|
||||
/>
|
||||
<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") }}
|
||||
{{ t("admin.settings.aliyunCaptcha.sceneIdHint") }}
|
||||
</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
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.accessKeyId") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_cloud_secret_id"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
v-model="form.aliyun_captcha_access_key_id"
|
||||
type="text"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
|
||||
placeholder="LTAI..."
|
||||
/>
|
||||
<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") }}
|
||||
{{ t("admin.settings.aliyunCaptcha.accessKeyIdHint") }}
|
||||
</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
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.accessKeySecret") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_cloud_secret_key"
|
||||
v-model="form.aliyun_captcha_access_key_secret"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<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") }}
|
||||
{{
|
||||
form.aliyun_captcha_access_key_secret_configured
|
||||
? t(
|
||||
"admin.settings.aliyunCaptcha.accessKeySecretConfiguredHint",
|
||||
)
|
||||
: t("admin.settings.aliyunCaptcha.accessKeySecretHint")
|
||||
}}
|
||||
</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>
|
||||
@@ -8990,6 +9126,7 @@ type SettingsForm = Omit<
|
||||
tencent_captcha_app_secret_key: string;
|
||||
tencent_captcha_cloud_secret_id: string;
|
||||
tencent_captcha_cloud_secret_key: string;
|
||||
aliyun_captcha_access_key_secret: string;
|
||||
linuxdo_connect_client_secret: string;
|
||||
dingtalk_connect_client_secret: string;
|
||||
wechat_connect_app_secret: string;
|
||||
@@ -9127,6 +9264,13 @@ const form = reactive<SettingsForm>({
|
||||
tencent_captcha_cloud_secret_id_configured: false,
|
||||
tencent_captcha_cloud_secret_key: "",
|
||||
tencent_captcha_cloud_secret_key_configured: false,
|
||||
aliyun_captcha_enabled: false,
|
||||
aliyun_captcha_access_key_id: "",
|
||||
aliyun_captcha_access_key_secret: "",
|
||||
aliyun_captcha_access_key_secret_configured: false,
|
||||
aliyun_captcha_scene_id: "",
|
||||
aliyun_captcha_prefix: "",
|
||||
aliyun_captcha_region: "cn",
|
||||
api_key_acl_trust_forwarded_ip: true,
|
||||
forwarded_client_ip_headers: [],
|
||||
// LinuxDo Connect OAuth 登录
|
||||
@@ -9290,12 +9434,40 @@ const form = reactive<SettingsForm>({
|
||||
allow_user_view_error_requests: false,
|
||||
});
|
||||
|
||||
function onTurnstileToggle(enabled: boolean): void {
|
||||
if (enabled) form.tencent_captcha_enabled = false;
|
||||
// 人机验证 UI 状态:单卡片「总开关 + 服务商单选」,落库仍是三个独立
|
||||
// enabled 键(与上游一致),由下面的映射保证同一时间至多一家启用。
|
||||
type CaptchaProviderSelection = "turnstile" | "tencent" | "aliyun";
|
||||
|
||||
const captchaProviderSelection = ref<CaptchaProviderSelection>("turnstile");
|
||||
|
||||
function applyCaptchaSelection(provider: CaptchaProviderSelection | null): void {
|
||||
form.turnstile_enabled = provider === "turnstile";
|
||||
form.tencent_captcha_enabled = provider === "tencent";
|
||||
form.aliyun_captcha_enabled = provider === "aliyun";
|
||||
}
|
||||
|
||||
function onTencentCaptchaToggle(enabled: boolean): void {
|
||||
if (enabled) form.turnstile_enabled = false;
|
||||
const captchaMasterEnabled = computed({
|
||||
get: () =>
|
||||
form.turnstile_enabled ||
|
||||
form.tencent_captcha_enabled ||
|
||||
form.aliyun_captcha_enabled,
|
||||
set: (enabled: boolean) =>
|
||||
applyCaptchaSelection(enabled ? captchaProviderSelection.value : null),
|
||||
});
|
||||
|
||||
function selectCaptchaProvider(provider: CaptchaProviderSelection): void {
|
||||
captchaProviderSelection.value = provider;
|
||||
applyCaptchaSelection(provider);
|
||||
}
|
||||
|
||||
function syncCaptchaProviderSelection(): void {
|
||||
if (form.tencent_captcha_enabled) {
|
||||
captchaProviderSelection.value = "tencent";
|
||||
} else if (form.aliyun_captcha_enabled) {
|
||||
captchaProviderSelection.value = "aliyun";
|
||||
} else if (form.turnstile_enabled) {
|
||||
captchaProviderSelection.value = "turnstile";
|
||||
}
|
||||
}
|
||||
|
||||
type OpenAIAdvancedSchedulerOverrideKey =
|
||||
@@ -10209,6 +10381,7 @@ async function loadSettings() {
|
||||
(form as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
}
|
||||
syncCaptchaProviderSelection();
|
||||
if (!form.claude_oauth_system_prompt_blocks?.trim()) {
|
||||
form.claude_oauth_system_prompt_blocks =
|
||||
defaultClaudeOAuthSystemPromptBlocks;
|
||||
@@ -10266,6 +10439,7 @@ async function loadSettings() {
|
||||
form.tencent_captcha_app_secret_key = "";
|
||||
form.tencent_captcha_cloud_secret_id = "";
|
||||
form.tencent_captcha_cloud_secret_key = "";
|
||||
form.aliyun_captcha_access_key_secret = "";
|
||||
form.linuxdo_connect_client_secret = "";
|
||||
form.dingtalk_connect_client_secret = "";
|
||||
form.github_oauth_client_secret = "";
|
||||
@@ -10643,6 +10817,13 @@ async function saveSettings() {
|
||||
form.tencent_captcha_cloud_secret_id || undefined,
|
||||
tencent_captcha_cloud_secret_key:
|
||||
form.tencent_captcha_cloud_secret_key || undefined,
|
||||
aliyun_captcha_enabled: form.aliyun_captcha_enabled,
|
||||
aliyun_captcha_access_key_id: form.aliyun_captcha_access_key_id,
|
||||
aliyun_captcha_access_key_secret:
|
||||
form.aliyun_captcha_access_key_secret || undefined,
|
||||
aliyun_captcha_scene_id: form.aliyun_captcha_scene_id,
|
||||
aliyun_captcha_prefix: form.aliyun_captcha_prefix,
|
||||
aliyun_captcha_region: form.aliyun_captcha_region,
|
||||
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,
|
||||
@@ -10933,6 +11114,7 @@ async function saveSettings() {
|
||||
form.smtp_password = "";
|
||||
smtpPasswordManuallyEdited.value = false;
|
||||
form.turnstile_secret_key = "";
|
||||
form.aliyun_captcha_access_key_secret = "";
|
||||
form.linuxdo_connect_client_secret = "";
|
||||
form.dingtalk_connect_client_secret = "";
|
||||
form.github_oauth_client_secret = "";
|
||||
|
||||
@@ -772,24 +772,24 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("腾讯天御验证码与 Turnstile 开关互斥并保存四项配置", async () => {
|
||||
it("人机验证切换到腾讯天御并保存四项配置", 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);
|
||||
const masterToggle = wrapper.get('[data-testid="captcha-enabled-toggle"]');
|
||||
await masterToggle.setValue(true);
|
||||
// 默认选中 Turnstile
|
||||
expect(wrapper.text()).toContain("admin.settings.turnstile.siteKey");
|
||||
|
||||
await tencentToggle.setValue(true);
|
||||
expect((turnstileToggle.element as HTMLInputElement).checked).toBe(false);
|
||||
expect((tencentToggle.element as HTMLInputElement).checked).toBe(true);
|
||||
await wrapper.get('[data-testid="captcha-provider-tencent"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
const card = wrapper
|
||||
.findAll(".card")
|
||||
.find((node) => node.text().includes("admin.settings.tencentCaptcha.title"));
|
||||
.find((node) => node.text().includes("admin.settings.captcha.title"));
|
||||
expect(card).toBeDefined();
|
||||
expect(card!.text()).not.toContain("admin.settings.turnstile.siteKey");
|
||||
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(
|
||||
@@ -808,6 +808,7 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
expect.objectContaining({
|
||||
turnstile_enabled: false,
|
||||
tencent_captcha_enabled: true,
|
||||
aliyun_captcha_enabled: false,
|
||||
tencent_captcha_app_id: "123456789",
|
||||
tencent_captcha_app_secret_key: "app-secret-value",
|
||||
tencent_captcha_cloud_secret_id: "cloud-secret-id-value",
|
||||
@@ -816,6 +817,77 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("人机验证切换到阿里云并保存配置", async () => {
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
await openSecurityTab(wrapper);
|
||||
|
||||
const masterToggle = wrapper.get('[data-testid="captcha-enabled-toggle"]');
|
||||
await masterToggle.setValue(true);
|
||||
|
||||
await wrapper.get('[data-testid="captcha-provider-aliyun"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
const card = wrapper
|
||||
.findAll(".card")
|
||||
.find((node) => node.text().includes("admin.settings.captcha.title"));
|
||||
expect(card).toBeDefined();
|
||||
expect(card!.text()).toContain("admin.settings.aliyunCaptcha.region");
|
||||
expect(card!.text()).not.toContain("admin.settings.turnstile.siteKey");
|
||||
const inputs = card!.findAll("input").filter((input) => input.attributes("type") !== "checkbox");
|
||||
await inputs[0]!.setValue("prefix-1");
|
||||
await inputs[1]!.setValue("scene-1");
|
||||
await inputs[2]!.setValue("ak-id");
|
||||
await inputs[3]!.setValue("ak-secret-value");
|
||||
|
||||
await wrapper.find("form").trigger("submit.prevent");
|
||||
await flushPromises();
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
turnstile_enabled: false,
|
||||
tencent_captcha_enabled: false,
|
||||
aliyun_captcha_enabled: true,
|
||||
aliyun_captcha_prefix: "prefix-1",
|
||||
aliyun_captcha_scene_id: "scene-1",
|
||||
aliyun_captcha_access_key_id: "ak-id",
|
||||
aliyun_captcha_access_key_secret: "ak-secret-value",
|
||||
aliyun_captcha_region: "cn",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("关闭人机验证总开关会同时关闭所有服务商", async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...baseSettingsResponse,
|
||||
tencent_captcha_enabled: true,
|
||||
tencent_captcha_app_id: "123456789",
|
||||
tencent_captcha_app_secret_key_configured: true,
|
||||
tencent_captcha_cloud_secret_id_configured: true,
|
||||
tencent_captcha_cloud_secret_key_configured: true,
|
||||
});
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
await openSecurityTab(wrapper);
|
||||
|
||||
const masterToggle = wrapper.get('[data-testid="captcha-enabled-toggle"]');
|
||||
expect((masterToggle.element as HTMLInputElement).checked).toBe(true);
|
||||
// 加载后选中项跟随已启用的服务商
|
||||
expect(wrapper.text()).toContain("admin.settings.tencentCaptcha.appId");
|
||||
|
||||
await masterToggle.setValue(false);
|
||||
await wrapper.find("form").trigger("submit.prevent");
|
||||
await flushPromises();
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
turnstile_enabled: false,
|
||||
tencent_captcha_enabled: false,
|
||||
aliyun_captcha_enabled: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("disables passkey sign-in when the RP configuration is unavailable", async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...baseSettingsResponse,
|
||||
|
||||
Reference in new issue
Block a user