mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 16:26:30 +00:00
fix: 完善腾讯验证码区域适配与 CSP 白名单
修复国内站和国际站 SDK 构造、验证容器、票据重置及动态资源加载问题,并补充认证流程回归测试。
This commit is contained in:
@@ -2108,6 +2108,42 @@
|
||||
|
||||
<!-- Tencent Captcha fields -->
|
||||
<div v-else-if="captchaProviderSelection === 'tencent'">
|
||||
<div class="mb-6 max-w-sm">
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.tencentCaptcha.region") }}
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-2 rounded-lg bg-gray-100 p-1 dark:bg-dark-700">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="tencent-captcha-region-cn"
|
||||
class="inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium transition"
|
||||
:class="
|
||||
form.tencent_captcha_region !== 'intl'
|
||||
? '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.tencent_captcha_region = 'cn'"
|
||||
>
|
||||
{{ t("admin.settings.tencentCaptcha.regionCn") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="tencent-captcha-region-intl"
|
||||
class="inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium transition"
|
||||
:class="
|
||||
form.tencent_captcha_region === 'intl'
|
||||
? '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.tencent_captcha_region = 'intl'"
|
||||
>
|
||||
{{ t("admin.settings.tencentCaptcha.regionIntl") }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.regionHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<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">
|
||||
@@ -2191,7 +2227,7 @@
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-sm">
|
||||
<a
|
||||
href="https://console.cloud.tencent.com/captcha"
|
||||
:href="tencentCaptchaLinks.console"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-600 hover:text-primary-500"
|
||||
@@ -2199,7 +2235,7 @@
|
||||
{{ t("admin.settings.tencentCaptcha.openCaptchaConsole") }}
|
||||
</a>
|
||||
<a
|
||||
href="https://console.cloud.tencent.com/cam/capi"
|
||||
:href="tencentCaptchaLinks.cloudKeys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-600 hover:text-primary-500"
|
||||
@@ -2207,7 +2243,7 @@
|
||||
{{ t("admin.settings.tencentCaptcha.createCloudKeys") }}
|
||||
</a>
|
||||
<a
|
||||
href="https://cloud.tencent.com/document/product/1110/36841"
|
||||
:href="tencentCaptchaLinks.webDocs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-600 hover:text-primary-500"
|
||||
@@ -9264,6 +9300,7 @@ const form = reactive<SettingsForm>({
|
||||
tencent_captcha_cloud_secret_id_configured: false,
|
||||
tencent_captcha_cloud_secret_key: "",
|
||||
tencent_captcha_cloud_secret_key_configured: false,
|
||||
tencent_captcha_region: "cn",
|
||||
aliyun_captcha_enabled: false,
|
||||
aliyun_captcha_access_key_id: "",
|
||||
aliyun_captcha_access_key_secret: "",
|
||||
@@ -9460,6 +9497,22 @@ function selectCaptchaProvider(provider: CaptchaProviderSelection): void {
|
||||
applyCaptchaSelection(provider);
|
||||
}
|
||||
|
||||
// 天御中国站与国际站是两套独立账号体系,控制台与文档入口不通用,
|
||||
// 按当前选择的站点给出对应链接,避免管理员在错误的控制台里找不到 CaptchaAppId。
|
||||
const tencentCaptchaLinks = computed(() =>
|
||||
form.tencent_captcha_region === "intl"
|
||||
? {
|
||||
console: "https://console.tencentcloud.com/captcha/graphical",
|
||||
cloudKeys: "https://console.tencentcloud.com/cam/capi",
|
||||
webDocs: "https://www.tencentcloud.com/document/product/1159/49680",
|
||||
}
|
||||
: {
|
||||
console: "https://console.cloud.tencent.com/captcha",
|
||||
cloudKeys: "https://console.cloud.tencent.com/cam/capi",
|
||||
webDocs: "https://cloud.tencent.com/document/product/1110/36841",
|
||||
},
|
||||
);
|
||||
|
||||
function syncCaptchaProviderSelection(): void {
|
||||
if (form.tencent_captcha_enabled) {
|
||||
captchaProviderSelection.value = "tencent";
|
||||
@@ -10817,6 +10870,7 @@ async function saveSettings() {
|
||||
form.tencent_captcha_cloud_secret_id || undefined,
|
||||
tencent_captcha_cloud_secret_key:
|
||||
form.tencent_captcha_cloud_secret_key || undefined,
|
||||
tencent_captcha_region: form.tencent_captcha_region,
|
||||
aliyun_captcha_enabled: form.aliyun_captcha_enabled,
|
||||
aliyun_captcha_access_key_id: form.aliyun_captcha_access_key_id,
|
||||
aliyun_captcha_access_key_secret:
|
||||
|
||||
@@ -813,6 +813,36 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
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",
|
||||
tencent_captcha_region: "cn",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("腾讯天御切换到国际站后保存站点并更新控制台入口", async () => {
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
await openSecurityTab(wrapper);
|
||||
|
||||
await wrapper.get('[data-testid="captcha-enabled-toggle"]').setValue(true);
|
||||
await wrapper.get('[data-testid="captcha-provider-tencent"]').trigger("click");
|
||||
await wrapper.get('[data-testid="tencent-captcha-region-intl"]').trigger("click");
|
||||
|
||||
const card = wrapper
|
||||
.findAll(".card")
|
||||
.find((node) => node.text().includes("admin.settings.captcha.title"));
|
||||
expect(card).toBeDefined();
|
||||
expect(card!.get('a[href="https://console.tencentcloud.com/captcha/graphical"]').exists()).toBe(
|
||||
true,
|
||||
);
|
||||
expect(card!.get('a[href="https://console.tencentcloud.com/cam/capi"]').exists()).toBe(true);
|
||||
|
||||
await wrapper.find("form").trigger("submit.prevent");
|
||||
await flushPromises();
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tencent_captcha_enabled: true,
|
||||
tencent_captcha_region: "intl",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:tencent-region="tencentCaptchaRegion"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
@@ -93,6 +94,7 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:tencent-region="tencentCaptchaRegion"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
@@ -262,6 +264,7 @@ const turnstileEnabled = ref<boolean>(false)
|
||||
const turnstileSiteKey = ref<string>('')
|
||||
const tencentCaptchaEnabled = ref<boolean>(false)
|
||||
const tencentCaptchaAppId = ref<string>('')
|
||||
const tencentCaptchaRegion = ref<string>('cn')
|
||||
const aliyunCaptchaEnabled = ref<boolean>(false)
|
||||
const aliyunCaptchaSceneId = ref<string>('')
|
||||
const aliyunCaptchaPrefix = ref<string>('')
|
||||
@@ -361,6 +364,7 @@ onMounted(async () => {
|
||||
turnstileSiteKey.value = settings.turnstile_site_key || ''
|
||||
tencentCaptchaEnabled.value = settings.tencent_captcha_enabled === true
|
||||
tencentCaptchaAppId.value = settings.tencent_captcha_app_id || ''
|
||||
tencentCaptchaRegion.value = settings.tencent_captcha_region || 'cn'
|
||||
aliyunCaptchaEnabled.value = settings.aliyun_captcha_enabled === true
|
||||
aliyunCaptchaSceneId.value = settings.aliyun_captcha_scene_id || ''
|
||||
aliyunCaptchaPrefix.value = settings.aliyun_captcha_prefix || ''
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:tencent-region="tencentCaptchaRegion"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
@@ -157,6 +158,7 @@ const turnstileEnabled = ref<boolean>(false)
|
||||
const turnstileSiteKey = ref<string>('')
|
||||
const tencentCaptchaEnabled = ref<boolean>(false)
|
||||
const tencentCaptchaAppId = ref<string>('')
|
||||
const tencentCaptchaRegion = ref<string>('cn')
|
||||
const aliyunCaptchaEnabled = ref<boolean>(false)
|
||||
const aliyunCaptchaSceneId = ref<string>('')
|
||||
const aliyunCaptchaPrefix = ref<string>('')
|
||||
@@ -209,6 +211,7 @@ onMounted(async () => {
|
||||
turnstileSiteKey.value = settings.turnstile_site_key || ''
|
||||
tencentCaptchaEnabled.value = settings.tencent_captcha_enabled === true
|
||||
tencentCaptchaAppId.value = settings.tencent_captcha_app_id || ''
|
||||
tencentCaptchaRegion.value = settings.tencent_captcha_region || 'cn'
|
||||
aliyunCaptchaEnabled.value = settings.aliyun_captcha_enabled === true
|
||||
aliyunCaptchaSceneId.value = settings.aliyun_captcha_scene_id || ''
|
||||
aliyunCaptchaPrefix.value = settings.aliyun_captcha_prefix || ''
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:tencent-region="tencentCaptchaRegion"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
@@ -273,6 +274,7 @@ const turnstileEnabled = ref<boolean>(false)
|
||||
const turnstileSiteKey = ref<string>('')
|
||||
const tencentCaptchaEnabled = ref<boolean>(false)
|
||||
const tencentCaptchaAppId = ref<string>('')
|
||||
const tencentCaptchaRegion = ref<string>('cn')
|
||||
const aliyunCaptchaEnabled = ref<boolean>(false)
|
||||
const aliyunCaptchaSceneId = ref<string>('')
|
||||
const aliyunCaptchaPrefix = ref<string>('')
|
||||
@@ -383,6 +385,7 @@ onMounted(async () => {
|
||||
turnstileSiteKey.value = settings.turnstile_site_key || ''
|
||||
tencentCaptchaEnabled.value = settings.tencent_captcha_enabled === true
|
||||
tencentCaptchaAppId.value = settings.tencent_captcha_app_id || ''
|
||||
tencentCaptchaRegion.value = settings.tencent_captcha_region || 'cn'
|
||||
aliyunCaptchaEnabled.value = settings.aliyun_captcha_enabled === true
|
||||
aliyunCaptchaSceneId.value = settings.aliyun_captcha_scene_id || ''
|
||||
aliyunCaptchaPrefix.value = settings.aliyun_captcha_prefix || ''
|
||||
|
||||
@@ -211,6 +211,7 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:tencent-region="tencentCaptchaRegion"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
@@ -392,6 +393,7 @@ const turnstileEnabled = ref<boolean>(false)
|
||||
const turnstileSiteKey = ref<string>('')
|
||||
const tencentCaptchaEnabled = ref<boolean>(false)
|
||||
const tencentCaptchaAppId = ref<string>('')
|
||||
const tencentCaptchaRegion = ref<string>('cn')
|
||||
const aliyunCaptchaEnabled = ref<boolean>(false)
|
||||
const aliyunCaptchaSceneId = ref<string>('')
|
||||
const aliyunCaptchaPrefix = ref<string>('')
|
||||
@@ -524,6 +526,7 @@ onMounted(async () => {
|
||||
turnstileSiteKey.value = settings.turnstile_site_key || ''
|
||||
tencentCaptchaEnabled.value = settings.tencent_captcha_enabled === true
|
||||
tencentCaptchaAppId.value = settings.tencent_captcha_app_id || ''
|
||||
tencentCaptchaRegion.value = settings.tencent_captcha_region || 'cn'
|
||||
aliyunCaptchaEnabled.value = settings.aliyun_captcha_enabled === true
|
||||
aliyunCaptchaSceneId.value = settings.aliyun_captcha_scene_id || ''
|
||||
aliyunCaptchaPrefix.value = settings.aliyun_captcha_prefix || ''
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ForgotPasswordView from '@/views/auth/ForgotPasswordView.vue'
|
||||
|
||||
const getPublicSettingsMock = vi.fn()
|
||||
const forgotPasswordMock = vi.fn()
|
||||
const verifyActionMock = vi.fn()
|
||||
const captchaResetMock = vi.fn()
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({ t: (key: string) => key })
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/stores', () => ({
|
||||
useAppStore: () => ({
|
||||
showError: vi.fn(),
|
||||
showSuccess: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/api/auth', () => ({
|
||||
getPublicSettings: (...args: unknown[]) => getPublicSettingsMock(...args),
|
||||
forgotPassword: (...args: unknown[]) => forgotPasswordMock(...args)
|
||||
}))
|
||||
|
||||
const CaptchaChallengeStub = defineComponent({
|
||||
props: {
|
||||
tencentEnabled: Boolean,
|
||||
tencentAppId: String,
|
||||
tencentRegion: String
|
||||
},
|
||||
setup(_, { expose }) {
|
||||
expose({
|
||||
verifyAction: verifyActionMock,
|
||||
reset: captchaResetMock
|
||||
})
|
||||
return () => h('div', { 'data-testid': 'captcha-challenge' })
|
||||
}
|
||||
})
|
||||
|
||||
function mountForgotPassword() {
|
||||
return mount(ForgotPasswordView, {
|
||||
global: {
|
||||
stubs: {
|
||||
AuthLayout: { template: '<div><slot /><slot name="footer" /></div>' },
|
||||
Icon: true,
|
||||
RouterLink: true,
|
||||
TurnstileWidget: CaptchaChallengeStub
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('忘记密码腾讯验证码门禁', () => {
|
||||
beforeEach(() => {
|
||||
getPublicSettingsMock.mockReset()
|
||||
forgotPasswordMock.mockReset()
|
||||
verifyActionMock.mockReset()
|
||||
captchaResetMock.mockReset()
|
||||
getPublicSettingsMock.mockResolvedValue({
|
||||
turnstile_enabled: false,
|
||||
turnstile_site_key: '',
|
||||
tencent_captcha_enabled: true,
|
||||
tencent_captcha_app_id: '123456789',
|
||||
tencent_captcha_region: 'intl',
|
||||
aliyun_captcha_enabled: false
|
||||
})
|
||||
verifyActionMock.mockResolvedValue({ token: 'ticket-value', randstr: '@rand-value' })
|
||||
forgotPasswordMock.mockResolvedValue({ message: 'ok' })
|
||||
})
|
||||
|
||||
it('把公开设置中的站点传给验证码组件', async () => {
|
||||
const wrapper = mountForgotPassword()
|
||||
await flushPromises()
|
||||
|
||||
const captcha = wrapper.findComponent(CaptchaChallengeStub)
|
||||
expect(captcha.props('tencentEnabled')).toBe(true)
|
||||
expect(captcha.props('tencentAppId')).toBe('123456789')
|
||||
expect(captcha.props('tencentRegion')).toBe('intl')
|
||||
})
|
||||
|
||||
it('提交前获取新票据并原样发送到忘记密码接口', async () => {
|
||||
const wrapper = mountForgotPassword()
|
||||
await flushPromises()
|
||||
await wrapper.get('#email').setValue('user@example.com')
|
||||
|
||||
await wrapper.get('form').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
expect(verifyActionMock).toHaveBeenCalledOnce()
|
||||
expect(forgotPasswordMock).toHaveBeenCalledWith({
|
||||
email: 'user@example.com',
|
||||
turnstile_token: undefined,
|
||||
tencent_captcha_ticket: 'ticket-value',
|
||||
tencent_captcha_randstr: '@rand-value'
|
||||
})
|
||||
expect(captchaResetMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user