diff --git a/packages/backend/src/captcha/captcha.controller.ts b/packages/backend/src/captcha/captcha.controller.ts new file mode 100644 index 0000000..06ac2fa --- /dev/null +++ b/packages/backend/src/captcha/captcha.controller.ts @@ -0,0 +1,49 @@ +import { Request, Response } from 'express'; +import { captchaService } from '../services/captcha.service'; + +interface VerifyCaptchaCredentialsBody { + provider: 'hcaptcha' | 'recaptcha'; + siteKey?: string; + secretKey?: string; +} + +export class CaptchaController { + async verifyCredentials( + request: Request<{}, {}, VerifyCaptchaCredentialsBody>, // Express Request type + reply: Response // Express Response type + ): Promise { + const { provider, siteKey, secretKey } = request.body; + + if (!provider || (provider !== 'hcaptcha' && provider !== 'recaptcha')) { + reply.status(400).json({ message: '无效的 CAPTCHA 提供商。' }); // Use .json for Express + return; + } + + if (!siteKey || !secretKey) { + let missingKeyMessage = `缺少 ${provider} 的 Site Key 或 Secret Key。`; + if (!siteKey && !secretKey) { + missingKeyMessage = `缺少 ${provider} 的 Site Key 和 Secret Key。`; + } else if (!siteKey) { + missingKeyMessage = `缺少 ${provider} 的 Site Key。`; + } else if (!secretKey) { + missingKeyMessage = `缺少 ${provider} 的 Secret Key。`; + } + reply.status(400).json({ message: missingKeyMessage }); // Use .json + return; + } + + try { + const isValid = await captchaService.verifyCredentials(provider, siteKey, secretKey); + if (isValid) { + reply.status(200).json({ message: 'CAPTCHA 凭据验证成功。' }); // Use .json + } else { + reply.status(400).json({ message: 'CAPTCHA 凭据验证失败。请检查您的 Site Key 和 Secret Key 是否正确,并确保服务器可以访问 CAPTCHA 服务提供商。' }); // Use .json + } + } catch (error: any) { + console.error(`[CaptchaController] 凭据验证时发生意外错误: ${error.message}`); + reply.status(500).json({ message: error.message || 'CAPTCHA 凭据验证时发生服务器内部错误。' }); // Use .json + } + } +} + +export const captchaController = new CaptchaController(); \ No newline at end of file diff --git a/packages/backend/src/captcha/captcha.routes.ts b/packages/backend/src/captcha/captcha.routes.ts new file mode 100644 index 0000000..bcf6829 --- /dev/null +++ b/packages/backend/src/captcha/captcha.routes.ts @@ -0,0 +1,11 @@ +import express, { Router } from 'express'; +import { captchaController } from './captcha.controller'; +// import { requireAuth } from '../auth/auth.middleware'; // 假设的认证中间件 + +const router: Router = express.Router(); + +// POST /api/v1/settings/captcha/verify (路径将由 index.ts 中的 app.use 指定) +// 如果需要认证,可以在这里添加中间件: router.post('/verify', requireAuth, captchaController.verifyCredentials); +router.post('/verify', captchaController.verifyCredentials); + +export default router; \ No newline at end of file diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 96ff7d9..c1a2f15 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -44,6 +44,7 @@ import sftpRouter from './sftp/sftp.routes'; import proxyRoutes from './proxies/proxies.routes'; import tagsRouter from './tags/tags.routes'; import settingsRoutes from './settings/settings.routes'; +import captchaRoutes from './captcha/captcha.routes'; // +++ Import CAPTCHA routes +++ import notificationRoutes from './notifications/notification.routes'; import auditRoutes from './audit/audit.routes'; import commandHistoryRoutes from './command-history/command-history.routes'; @@ -264,6 +265,7 @@ const startServer = () => { app.use('/api/v1/proxies', proxyRoutes); app.use('/api/v1/tags', tagsRouter); app.use('/api/v1/settings', settingsRoutes); + app.use('/api/v1/settings/captcha', captchaRoutes); // +++ Register CAPTCHA routes under settings +++ app.use('/api/v1/notifications', notificationRoutes); app.use('/api/v1/audit-logs', auditRoutes); app.use('/api/v1/command-history', commandHistoryRoutes); diff --git a/packages/backend/src/services/captcha.service.ts b/packages/backend/src/services/captcha.service.ts index 4505ee3..bc3b55c 100644 --- a/packages/backend/src/services/captcha.service.ts +++ b/packages/backend/src/services/captcha.service.ts @@ -47,37 +47,107 @@ export class CaptchaService { } } + /** + * 验证提供的 CAPTCHA 凭据 (Site Key 和 Secret Key)。 + * @param provider - CAPTCHA 提供商 ('hcaptcha' 或 'recaptcha') + * @param siteKey - Site Key + * @param secretKey - Secret Key + * @returns Promise - 凭据是否有效 + * @throws Error 如果提供商不受支持或验证请求失败 + */ + async verifyCredentials(provider: 'hcaptcha' | 'recaptcha', siteKey: string, secretKey: string): Promise { + if (!siteKey || !secretKey) { + console.warn(`[CaptchaService] 凭据验证失败:${provider} 的 Site Key 或 Secret Key 为空。`); + return false; + } + + // 使用一个固定的、已知的无效令牌或一个不太可能有效的测试令牌 + const testToken = 'static_test_token_for_credential_verification_NexusTerminal'; + + console.log(`[CaptchaService] 正在验证 ${provider} 凭据 (SiteKey: ${siteKey.substring(0, 5)}...)`); + + try { + let success = false; + if (provider === 'hcaptcha') { + success = await this._verifyHCaptcha(testToken, secretKey, siteKey, true); + } else if (provider === 'recaptcha') { + success = await this._verifyReCaptcha(testToken, secretKey, siteKey, true); + } else { + throw new Error(`不支持的 CAPTCHA 提供商: ${provider}`); + } + return success; + } catch (error: any) { + // _verifyHCaptcha/_verifyReCaptcha 在凭据检查模式下会抛出特定错误 + console.error(`[CaptchaService] ${provider} 凭据验证期间发生错误:`, error.message); + return false; // 任何在验证方法内部捕获并重新抛出的错误都意味着凭据无效 + } + } + + /** * 调用 hCaptcha API 验证令牌。 * @param token - h-captcha-response 令牌 * @param secretKey - hCaptcha Secret Key - * @returns Promise - 令牌是否有效 + * @param siteKey - (可选) hCaptcha Site Key, 用于凭据验证模式 + * @param isCredentialVerification - (可选) 是否为凭据验证模式 + * @returns Promise - 令牌/凭据是否有效 */ - private async _verifyHCaptcha(token: string, secretKey: string): Promise { - console.log('[CaptchaService] 正在验证 hCaptcha 令牌...'); + private async _verifyHCaptcha(token: string, secretKey: string, siteKey?: string, isCredentialVerification = false): Promise { + const mode = isCredentialVerification ? "凭据" : "令牌"; + console.log(`[CaptchaService] 正在验证 hCaptcha ${mode}...`); try { - // 正确方式:将数据放在 POST body 中,并使用 URLSearchParams 格式化 const params = new URLSearchParams(); params.append('secret', secretKey); params.append('response', token); - // params.append('remoteip', userIpAddress); // 如果需要传递用户 IP + if (siteKey) { // hCaptcha 的 siteverify 也接受 sitekey 参数 + params.append('sitekey', siteKey); + } const response = await axios.post(HCAPTCHA_VERIFY_URL, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); - console.log('[CaptchaService] hCaptcha 验证响应:', response.data); + console.log(`[CaptchaService] hCaptcha ${mode}验证响应:`, response.data); + const errorCodes: string[] = response.data['error-codes'] || []; + if (response.data && response.data.success === true) { - console.log('[CaptchaService] hCaptcha 令牌验证成功。'); + console.log(`[CaptchaService] hCaptcha ${mode}验证成功。`); return true; } else { - console.warn('[CaptchaService] hCaptcha 令牌验证失败:', response.data['error-codes'] || '未知错误'); - return false; + console.warn(`[CaptchaService] hCaptcha ${mode}验证失败:`, errorCodes.join(', ') || '未知错误'); + if (isCredentialVerification) { + // 对于凭据验证,如果错误不是关于密钥本身的,我们可能仍然认为密钥“可能”有效。 + // 关键的错误代码是 'invalid-input-secret'。'invalid-sitekey' 也是一个明确的凭据错误。 + // 其他错误,如 'missing-input-response', 'invalid-input-response' 是关于测试令牌的,可以忽略。 + if (errorCodes.includes('invalid-input-secret') || errorCodes.includes('invalid-sitekey')) { + throw new Error(`hCaptcha 凭据无效: ${errorCodes.join(', ')}`); + } + // 如果没有明确的密钥错误,并且不是成功,对于凭据验证,这仍可能意味着密钥组合是“可查询的” + // 但为了更严格,我们也可以返回 false,或根据具体错误代码决定。 + // 此处,如果不是特定的密钥错误,我们乐观地认为凭据本身“可能”没问题,只是测试令牌无效。 + // 然而,更安全的方式是,任何非 success 都视为凭据验证失败,除非 API 设计允许区分。 + // 为了符合“校验成功才能保存”,这里如果 success 为 false,即使没有特定密钥错误,也应该返回 false + // 或者抛出错误让上层决定。我们在此处抛出,由 verifyCredentials 捕获并返回 false. + if (errorCodes.length > 0 && !errorCodes.includes('invalid-input-secret') && !errorCodes.includes('invalid-sitekey')) { + // 例如: 'invalid-input-response', 'sitekey-secret-mismatch' (如果 sitekey 和 secret 不匹配但格式正确) + // 'sitekey-secret-mismatch' 也是一个凭据问题 + if (errorCodes.includes('sitekey-secret-mismatch')) { + throw new Error(`hCaptcha 凭据无效: sitekey 与 secret 不匹配`); + } + // 如果是 'invalid-input-response' 这类关于测试令牌的错误,我们认为密钥“可能”是对的。 + // 但前端期望布尔值,如果不是 success:true,这里就返回false,表示“未严格验证通过” + console.warn(`[CaptchaService] hCaptcha ${mode}验证失败,但错误可能与测试令牌有关而非密钥本身: ${errorCodes.join(', ')}`); + return false; // 对于凭据验证,如果不是true,就严格返回false + } + return false; // 其他所有情况的失败 + } + return false; // 令牌验证失败 } } catch (error: any) { - console.error('[CaptchaService] 调用 hCaptcha 验证 API 时出错:', error.response?.data || error.message); - // 抛出错误,让上层处理(例如,提示用户稍后重试) - throw new Error(`hCaptcha 验证请求失败: ${error.message}`); + const errorMessage = error.response?.data?.message || error.message || '未知网络错误'; + console.error(`[CaptchaService] 调用 hCaptcha ${mode}验证 API 时出错:`, errorMessage, error.response?.data || ''); + // 抛出错误,让上层处理 + throw new Error(`hCaptcha ${mode}验证请求失败: ${errorMessage}`); } } @@ -85,33 +155,51 @@ export class CaptchaService { * 调用 Google reCAPTCHA API 验证令牌。 * @param token - g-recaptcha-response 令牌 * @param secretKey - Google reCAPTCHA Secret Key - * @returns Promise - 令牌是否有效 + * @param siteKey - (可选) Google reCAPTCHA Site Key, reCAPTCHA 的 siteverify 不直接使用 sitekey 作为参数,但保留以保持接口一致性 + * @param isCredentialVerification - (可选) 是否为凭据验证模式 + * @returns Promise - 令牌/凭据是否有效 */ - private async _verifyReCaptcha(token: string, secretKey: string): Promise { - console.log('[CaptchaService] 正在验证 Google reCAPTCHA 令牌...'); + private async _verifyReCaptcha(token: string, secretKey: string, siteKey?: string, isCredentialVerification = false): Promise { + const mode = isCredentialVerification ? "凭据" : "令牌"; + console.log(`[CaptchaService] 正在验证 Google reCAPTCHA ${mode}... (SiteKey: ${siteKey ? siteKey.substring(0,5)+'...' : 'N/A'})`); try { - // 正确方式:将数据放在 POST body 中,并使用 URLSearchParams 格式化 const params = new URLSearchParams(); params.append('secret', secretKey); params.append('response', token); + // reCAPTCHA 的 siteverify API 不像 hCaptcha 那样直接接受 sitekey 参数 + // sitekey 的验证是隐式通过 secretKey 的。 const response = await axios.post(RECAPTCHA_VERIFY_URL, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); - console.log('[CaptchaService] Google reCAPTCHA 验证响应:', response.data); + console.log(`[CaptchaService] Google reCAPTCHA ${mode}验证响应:`, response.data); + const errorCodes: string[] = response.data['error-codes'] || []; + if (response.data && response.data.success === true) { - // 可选:检查 hostname, score (v3), action (v3) 等 - console.log('[CaptchaService] Google reCAPTCHA 令牌验证成功。'); + console.log(`[CaptchaService] Google reCAPTCHA ${mode}验证成功。`); return true; } else { - console.warn('[CaptchaService] Google reCAPTCHA 令牌验证失败:', response.data['error-codes'] || '未知错误'); - return false; + console.warn(`[CaptchaService] Google reCAPTCHA ${mode}验证失败:`, errorCodes.join(', ') || '未知错误'); + if (isCredentialVerification) { + // 对于凭据验证,关注与密钥相关的错误 + // 例如: 'invalid-input-secret', 'bad-request' (有时可能由错误的密钥导致), 'invalid-keys' (如果API支持) + // 'missing-input-response', 'invalid-input-response' 是关于测试令牌的。 + if (errorCodes.includes('invalid-input-secret') || errorCodes.includes('invalid-keys') /* hypothetical */) { + throw new Error(`Google reCAPTCHA 凭据无效: ${errorCodes.join(', ')}`); + } + // 如果是 'missing-input-response', 'invalid-input-response' + // reCAPTCHA 倾向于对无效密钥返回 success: false 和 "invalid-input-secret" + // 如果没有明确的密钥错误,并且不是 success,严格返回 false + console.warn(`[CaptchaService] Google reCAPTCHA ${mode}验证失败,但错误可能与测试令牌有关而非密钥本身: ${errorCodes.join(', ')}`); + return false; // 对于凭据验证,如果不是true,就严格返回false + } + return false; // 令牌验证失败 } } catch (error: any) { - console.error('[CaptchaService] 调用 Google reCAPTCHA 验证 API 时出错:', error.response?.data || error.message); - // 抛出错误,让上层处理 - throw new Error(`Google reCAPTCHA 验证请求失败: ${error.message}`); + const errorMessage = error.response?.data?.message || error.message || '未知网络错误'; + console.error(`[CaptchaService] 调用 Google reCAPTCHA ${mode}验证 API 时出错:`, errorMessage, error.response?.data || ''); + throw new Error(`Google reCAPTCHA ${mode}验证请求失败: ${errorMessage}`); } } } diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index a287af5..6b387b1 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -808,7 +808,10 @@ "saved": "CAPTCHA settings saved successfully." }, "error": { - "saveFailed": "Failed to save CAPTCHA settings." + "saveFailed": "Failed to save CAPTCHA settings.", + "hcaptchaKeysRequired": "hCaptcha Site Key and Secret Key are required for verification.", + "recaptchaKeysRequired": "reCAPTCHA Site Key and Secret Key are required for verification.", + "verificationFailed": "CAPTCHA credential verification failed. Please check your Site Key and Secret Key." } }, "commandInputSync": { diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index b1781c1..331fb09 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -691,7 +691,10 @@ "description": "自動化された攻撃を防ぐために、ログインページに CAPTCHA 検証を設定します。", "enableLabel": "ログインページで CAPTCHA を有効にする", "error": { - "saveFailed": "CAPTCHA 設定の保存に失敗しました。" + "saveFailed": "CAPTCHA 設定の保存に失敗しました。", + "hcaptchaKeysRequired": "hCaptcha サイトキーとシークレットキーは検証に必要です。", + "recaptchaKeysRequired": "reCAPTCHA サイトキーとシークレットキーは検証に必要です。", + "verificationFailed": "CAPTCHA 認証情報検証に失敗しました。サイトキーとシークレットキーを確認してください。" }, "hcaptchaHint": "から", "providerLabel": "CAPTCHA プロバイダー:", diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index f27e77d..48f1034 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -807,7 +807,10 @@ "saved": "CAPTCHA 设置已成功保存。" }, "error": { - "saveFailed": "保存 CAPTCHA 设置失败。" + "saveFailed": "保存 CAPTCHA 设置失败。", + "hcaptchaKeysRequired": "hCaptcha Site Key 和 Secret Key 是必填项,以便进行验证。", + "recaptchaKeysRequired": "reCAPTCHA Site Key 和 Secret Key 是必填项,以便进行验证。", + "verificationFailed": "CAPTCHA 凭据验证失败。请检查您输入的 Site Key 和 Secret Key 是否正确。" } }, "commandInputSync": { diff --git a/packages/frontend/src/stores/auth.store.ts b/packages/frontend/src/stores/auth.store.ts index 54b2564..5731a6d 100644 --- a/packages/frontend/src/stores/auth.store.ts +++ b/packages/frontend/src/stores/auth.store.ts @@ -12,7 +12,7 @@ interface UserInfo { } // Passkey Information Interface -interface PasskeyInfo { +export interface PasskeyInfo { // + Export 接口 credentialID: string; publicKey: string; // Or a more specific type if available counter: number; diff --git a/packages/frontend/src/views/SettingsView.vue b/packages/frontend/src/views/SettingsView.vue index be74f22..6a098bd 100644 --- a/packages/frontend/src/views/SettingsView.vue +++ b/packages/frontend/src/views/SettingsView.vue @@ -1654,32 +1654,125 @@ const handleUpdateCaptchaSettings = async () => { captchaMessage.value = ''; captchaSuccess.value = false; try { - // Prepare DTO, only sending secret keys if they have been entered - const dto: UpdateCaptchaSettingsDto = { + let needsVerification = false; + let providerForVerification: CaptchaProvider | null = null; + let siteKeyForVerification: string | undefined = undefined; + let secretKeyForVerification: string | undefined = undefined; + + // 步骤 1: 确定是否需要验证 + if (captchaForm.enabled && captchaForm.provider && captchaForm.provider !== 'none') { + const originalSettings = captchaSettings.value; // 从 store 获取的持久化设置 + + if (captchaForm.provider === 'hcaptcha') { + const originalSiteKeyValue = originalSettings?.hcaptchaSiteKey || ''; + const currentSiteKeyValue = captchaForm.hcaptchaSiteKey || ''; + const currentSecretKeyValue = captchaForm.hcaptchaSecretKey || ''; // 用户在表单中输入的新 secret + + if (currentSiteKeyValue !== originalSiteKeyValue) { // 情况 A: 站点密钥已更改 + if (!currentSiteKeyValue || !currentSecretKeyValue) { + captchaMessage.value = t('settings.captcha.error.hcaptchaKeysRequired'); + captchaSuccess.value = false; + captchaLoading.value = false; + return; + } + needsVerification = true; + providerForVerification = 'hcaptcha'; + siteKeyForVerification = currentSiteKeyValue; + secretKeyForVerification = currentSecretKeyValue; + } else if (currentSecretKeyValue) { // 情况 B: 站点密钥未更改,但用户输入了新的秘密密钥 + if (!currentSiteKeyValue) { // 确保站点密钥本身不为空 + captchaMessage.value = t('settings.captcha.error.hcaptchaKeysRequired'); + captchaSuccess.value = false; + captchaLoading.value = false; + return; + } + needsVerification = true; + providerForVerification = 'hcaptcha'; + siteKeyForVerification = currentSiteKeyValue; // 使用当前的 (也是原始的) 站点密钥 + secretKeyForVerification = currentSecretKeyValue; + } + // 情况 C: 站点密钥未更改,且用户未输入新的秘密密钥 (currentSecretKeyValue is empty) -> needsVerification 保持 false + } else if (captchaForm.provider === 'recaptcha') { + const originalSiteKeyValue = originalSettings?.recaptchaSiteKey || ''; + const currentSiteKeyValue = captchaForm.recaptchaSiteKey || ''; + const currentSecretKeyValue = captchaForm.recaptchaSecretKey || ''; + + if (currentSiteKeyValue !== originalSiteKeyValue) { // 情况 A: 站点密钥已更改 + if (!currentSiteKeyValue || !currentSecretKeyValue) { + captchaMessage.value = t('settings.captcha.error.recaptchaKeysRequired'); + captchaSuccess.value = false; + captchaLoading.value = false; + return; + } + needsVerification = true; + providerForVerification = 'recaptcha'; + siteKeyForVerification = currentSiteKeyValue; + secretKeyForVerification = currentSecretKeyValue; + } else if (currentSecretKeyValue) { // 情况 B: 站点密钥未更改,但用户输入了新的秘密密钥 + if (!currentSiteKeyValue) { // 确保站点密钥本身不为空 + captchaMessage.value = t('settings.captcha.error.recaptchaKeysRequired'); + captchaSuccess.value = false; + captchaLoading.value = false; + return; + } + needsVerification = true; + providerForVerification = 'recaptcha'; + siteKeyForVerification = currentSiteKeyValue; + secretKeyForVerification = currentSecretKeyValue; + } + // 情况 C: 站点密钥未更改,且用户未输入新的秘密密钥 -> needsVerification 保持 false + } + } + + // 步骤 2: 如果需要,执行验证 + if (needsVerification && providerForVerification && siteKeyForVerification && secretKeyForVerification) { + try { + await apiClient.post('/settings/captcha/verify', { + provider: providerForVerification, + siteKey: siteKeyForVerification, + secretKey: secretKeyForVerification, + }); + // 验证成功,可以继续 + } catch (verifyError: any) { + console.error('CAPTCHA 验证失败:', verifyError); + captchaMessage.value = verifyError.response?.data?.message || verifyError.message || t('settings.captcha.error.verificationFailed'); + captchaSuccess.value = false; + captchaLoading.value = false; + return; // 验证失败,不继续保存 + } + } + + // 步骤 3: 准备用于保存的 DTO + const dtoToSave: UpdateCaptchaSettingsDto = { enabled: captchaForm.enabled, provider: captchaForm.provider, - // Site keys are not sensitive, send them if present - hcaptchaSiteKey: captchaForm.hcaptchaSiteKey || '', - recaptchaSiteKey: captchaForm.recaptchaSiteKey || '', + // Site keys 总是从表单获取 + hcaptchaSiteKey: captchaForm.hcaptchaSiteKey || undefined, + recaptchaSiteKey: captchaForm.recaptchaSiteKey || undefined, + // Secret keys 仅在表单中提供时才包含 + hcaptchaSecretKey: captchaForm.hcaptchaSecretKey || undefined, + recaptchaSecretKey: captchaForm.recaptchaSecretKey || undefined, }; + // 如果 captchaForm.provider 为 'none' 或 captchaForm.enabled 为 false, + // 后端应负责清除所有相关的 site/secret key。 + // 如果表单中的 secret key 为空字符串,则发送 undefined,后端不应更新该特定 secret key。 - // Only include secret keys in the DTO if the user entered a value - if (captchaForm.hcaptchaSecretKey) { - dto.hcaptchaSecretKey = captchaForm.hcaptchaSecretKey; - } - if (captchaForm.recaptchaSecretKey) { - dto.recaptchaSecretKey = captchaForm.recaptchaSecretKey; - } - - await settingsStore.updateCaptchaSettings(dto); - captchaMessage.value = t('settings.captcha.success.saved'); // Need translation + // 步骤 4: 调用保存操作 + await settingsStore.updateCaptchaSettings(dtoToSave); + captchaMessage.value = t('settings.captcha.success.saved'); captchaSuccess.value = true; - // Clear secret key fields in the form after successful save for security + // 成功保存后清除表单中的 secret key 字段,以确保下次编辑时它们是空的,除非用户再次输入 captchaForm.hcaptchaSecretKey = ''; captchaForm.recaptchaSecretKey = ''; + } catch (error: any) { - console.error('更新 CAPTCHA 设置失败:', error); - captchaMessage.value = error.message || t('settings.captcha.error.saveFailed'); // Need translation + // 此 catch 块处理来自 settingsStore.updateCaptchaSettings 的错误 + // 或在 try 块中未被 'return' 语句捕获的其他错误。 + console.error('更新 CAPTCHA 设置时捕获到错误:', error); + // 避免覆盖更具体的错误消息(例如,来自验证失败的消息) + if (!captchaMessage.value) { + captchaMessage.value = error.message || t('settings.captcha.error.saveFailed'); + } captchaSuccess.value = false; } finally { captchaLoading.value = false;