update
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import axios from 'axios';
|
||||
import { settingsService } from './settings.service';
|
||||
import { CaptchaSettings, CaptchaProvider } from '../types/settings.types';
|
||||
|
||||
// CAPTCHA 验证 API 端点
|
||||
const HCAPTCHA_VERIFY_URL = 'https://api.hcaptcha.com/siteverify';
|
||||
const RECAPTCHA_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'; // v2
|
||||
|
||||
export class CaptchaService {
|
||||
|
||||
/**
|
||||
* 验证提供的 CAPTCHA 令牌。
|
||||
* 根据系统设置自动选择合适的提供商进行验证。
|
||||
* @param token - 从前端获取的 CAPTCHA 令牌 (h-captcha-response 或 g-recaptcha-response)
|
||||
* @returns Promise<boolean> - 令牌是否有效
|
||||
* @throws Error 如果配置无效或验证请求失败
|
||||
*/
|
||||
async verifyToken(token: string): Promise<boolean> {
|
||||
if (!token) {
|
||||
console.warn('[CaptchaService] 验证失败:未提供令牌。');
|
||||
return false; // 没有令牌,直接视为无效
|
||||
}
|
||||
|
||||
const captchaConfig = await settingsService.getCaptchaConfig();
|
||||
|
||||
if (!captchaConfig.enabled) {
|
||||
console.log('[CaptchaService] CAPTCHA 未启用,跳过验证。');
|
||||
return true; // 未启用则视为验证通过
|
||||
}
|
||||
|
||||
switch (captchaConfig.provider) {
|
||||
case 'hcaptcha':
|
||||
if (!captchaConfig.hcaptchaSecretKey) {
|
||||
throw new Error('hCaptcha 配置无效:缺少 Secret Key。');
|
||||
}
|
||||
return this._verifyHCaptcha(token, captchaConfig.hcaptchaSecretKey);
|
||||
case 'recaptcha':
|
||||
if (!captchaConfig.recaptchaSecretKey) {
|
||||
throw new Error('Google reCAPTCHA 配置无效:缺少 Secret Key。');
|
||||
}
|
||||
return this._verifyReCaptcha(token, captchaConfig.recaptchaSecretKey);
|
||||
case 'none':
|
||||
console.log('[CaptchaService] CAPTCHA 提供商设置为 "none",跳过验证。');
|
||||
return true; // 提供商为 none 也视为通过
|
||||
default:
|
||||
console.error(`[CaptchaService] 未知的 CAPTCHA 提供商: ${captchaConfig.provider}`);
|
||||
throw new Error(`未知的 CAPTCHA 提供商配置: ${captchaConfig.provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 hCaptcha API 验证令牌。
|
||||
* @param token - h-captcha-response 令牌
|
||||
* @param secretKey - hCaptcha Secret Key
|
||||
* @returns Promise<boolean> - 令牌是否有效
|
||||
*/
|
||||
private async _verifyHCaptcha(token: string, secretKey: string): Promise<boolean> {
|
||||
console.log('[CaptchaService] 正在验证 hCaptcha 令牌...');
|
||||
try {
|
||||
const response = await axios.post(HCAPTCHA_VERIFY_URL, null, { // 使用 POST,数据在 params 中
|
||||
params: {
|
||||
secret: secretKey,
|
||||
response: token,
|
||||
// remoteip: 可选,用户 IP 地址
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
});
|
||||
|
||||
console.log('[CaptchaService] hCaptcha 验证响应:', response.data);
|
||||
if (response.data && response.data.success === true) {
|
||||
console.log('[CaptchaService] hCaptcha 令牌验证成功。');
|
||||
return true;
|
||||
} else {
|
||||
console.warn('[CaptchaService] hCaptcha 令牌验证失败:', response.data['error-codes'] || '未知错误');
|
||||
return false;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[CaptchaService] 调用 hCaptcha 验证 API 时出错:', error.response?.data || error.message);
|
||||
// 抛出错误,让上层处理(例如,提示用户稍后重试)
|
||||
throw new Error(`hCaptcha 验证请求失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 Google reCAPTCHA API 验证令牌。
|
||||
* @param token - g-recaptcha-response 令牌
|
||||
* @param secretKey - Google reCAPTCHA Secret Key
|
||||
* @returns Promise<boolean> - 令牌是否有效
|
||||
*/
|
||||
private async _verifyReCaptcha(token: string, secretKey: string): Promise<boolean> {
|
||||
console.log('[CaptchaService] 正在验证 Google reCAPTCHA 令牌...');
|
||||
try {
|
||||
const response = await axios.post(RECAPTCHA_VERIFY_URL, null, { // 使用 POST,数据在 params 中
|
||||
params: {
|
||||
secret: secretKey,
|
||||
response: token,
|
||||
// remoteip: 可选,用户 IP 地址
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
});
|
||||
|
||||
console.log('[CaptchaService] Google reCAPTCHA 验证响应:', response.data);
|
||||
if (response.data && response.data.success === true) {
|
||||
// 可选:检查 hostname, score (v3), action (v3) 等
|
||||
console.log('[CaptchaService] Google reCAPTCHA 令牌验证成功。');
|
||||
return true;
|
||||
} else {
|
||||
console.warn('[CaptchaService] Google reCAPTCHA 令牌验证失败:', response.data['error-codes'] || '未知错误');
|
||||
return false;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[CaptchaService] 调用 Google reCAPTCHA 验证 API 时出错:', error.response?.data || error.message);
|
||||
// 抛出错误,让上层处理
|
||||
throw new Error(`Google reCAPTCHA 验证请求失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出一个单例供其他服务使用
|
||||
export const captchaService = new CaptchaService();
|
||||
@@ -1,5 +1,19 @@
|
||||
import { settingsRepository, Setting, getSidebarConfig as getSidebarConfigFromRepo, setSidebarConfig as setSidebarConfigInRepo } from '../repositories/settings.repository'; // Import specific repo functions
|
||||
import { SidebarConfig, PaneName, UpdateSidebarConfigDto } from '../types/settings.types';
|
||||
import {
|
||||
settingsRepository,
|
||||
Setting,
|
||||
getSidebarConfig as getSidebarConfigFromRepo,
|
||||
setSidebarConfig as setSidebarConfigInRepo,
|
||||
getCaptchaConfig as getCaptchaConfigFromRepo, // <-- Import CAPTCHA repo getter
|
||||
setCaptchaConfig as setCaptchaConfigInRepo, // <-- Import CAPTCHA repo setter
|
||||
} from '../repositories/settings.repository';
|
||||
import {
|
||||
SidebarConfig,
|
||||
PaneName,
|
||||
UpdateSidebarConfigDto,
|
||||
CaptchaSettings, // <-- Import CAPTCHA types
|
||||
UpdateCaptchaSettingsDto, // <-- Import CAPTCHA types
|
||||
CaptchaProvider, // <-- Import CAPTCHA types
|
||||
} from '../types/settings.types';
|
||||
|
||||
// +++ 定义焦点切换完整配置接口 (与前端 store 保持一致) +++
|
||||
interface FocusItemConfig { // 单个项目的配置
|
||||
@@ -378,6 +392,76 @@ export const settingsService = {
|
||||
// Directly call the specific repository function
|
||||
await setSidebarConfigInRepo(configToSave);
|
||||
console.log('[SettingsService] Sidebar config successfully set.');
|
||||
} // <-- No comma after the last method in the object
|
||||
}, // <-- Add comma here
|
||||
|
||||
// --- CAPTCHA Settings Specific Functions ---
|
||||
|
||||
/**
|
||||
* 获取 CAPTCHA 配置
|
||||
* @returns Promise<CaptchaSettings>
|
||||
*/
|
||||
async getCaptchaConfig(): Promise<CaptchaSettings> {
|
||||
console.log('[SettingsService] Getting CAPTCHA config...');
|
||||
// Directly call the specific repository function
|
||||
const config = await getCaptchaConfigFromRepo();
|
||||
// Mask secret keys before logging
|
||||
const maskedConfig = { ...config, hcaptchaSecretKey: '***', recaptchaSecretKey: '***' };
|
||||
console.log('[SettingsService] Returning CAPTCHA config:', maskedConfig);
|
||||
return config;
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置 CAPTCHA 配置
|
||||
* @param configDto - The CAPTCHA configuration object from DTO
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async setCaptchaConfig(configDto: UpdateCaptchaSettingsDto): Promise<void> {
|
||||
console.log('[SettingsService] Setting CAPTCHA config (DTO):', { ...configDto, hcaptchaSecretKey: '***', recaptchaSecretKey: '***' }); // Mask secrets in log
|
||||
|
||||
// --- Validation ---
|
||||
if (!configDto || typeof configDto !== 'object') {
|
||||
throw new Error('无效的 CAPTCHA 配置格式。');
|
||||
}
|
||||
|
||||
// Fetch the current settings to merge with the DTO
|
||||
const currentConfig = await getCaptchaConfigFromRepo();
|
||||
const configToSave: CaptchaSettings = { ...currentConfig };
|
||||
|
||||
// Validate and update individual fields from DTO
|
||||
if (configDto.enabled !== undefined) {
|
||||
if (typeof configDto.enabled !== 'boolean') throw new Error('captcha.enabled 必须是布尔值。');
|
||||
configToSave.enabled = configDto.enabled;
|
||||
}
|
||||
if (configDto.provider !== undefined) {
|
||||
const validProviders: CaptchaProvider[] = ['hcaptcha', 'recaptcha', 'none'];
|
||||
if (!validProviders.includes(configDto.provider)) throw new Error(`无效的 CAPTCHA 提供商: ${configDto.provider}`);
|
||||
configToSave.provider = configDto.provider;
|
||||
}
|
||||
if (configDto.hcaptchaSiteKey !== undefined) {
|
||||
if (typeof configDto.hcaptchaSiteKey !== 'string') throw new Error('hcaptchaSiteKey 必须是字符串。');
|
||||
configToSave.hcaptchaSiteKey = configDto.hcaptchaSiteKey;
|
||||
}
|
||||
if (configDto.hcaptchaSecretKey !== undefined) {
|
||||
if (typeof configDto.hcaptchaSecretKey !== 'string') throw new Error('hcaptchaSecretKey 必须是字符串。');
|
||||
configToSave.hcaptchaSecretKey = configDto.hcaptchaSecretKey;
|
||||
}
|
||||
if (configDto.recaptchaSiteKey !== undefined) {
|
||||
if (typeof configDto.recaptchaSiteKey !== 'string') throw new Error('recaptchaSiteKey 必须是字符串。');
|
||||
configToSave.recaptchaSiteKey = configDto.recaptchaSiteKey;
|
||||
}
|
||||
if (configDto.recaptchaSecretKey !== undefined) {
|
||||
if (typeof configDto.recaptchaSecretKey !== 'string') throw new Error('recaptchaSecretKey 必须是字符串。');
|
||||
configToSave.recaptchaSecretKey = configDto.recaptchaSecretKey;
|
||||
}
|
||||
|
||||
// Ensure consistency: if disabled, provider should ideally be 'none' (optional enforcement)
|
||||
// if (!configToSave.enabled) {
|
||||
// configToSave.provider = 'none';
|
||||
// }
|
||||
|
||||
// Directly call the specific repository function with the full, validated config
|
||||
await setCaptchaConfigInRepo(configToSave);
|
||||
console.log('[SettingsService] CAPTCHA config successfully set.');
|
||||
} // <-- No comma after the last method
|
||||
|
||||
}; // <-- End of settingsService object definition
|
||||
|
||||
Reference in New Issue
Block a user