This commit is contained in:
Baobhan Sith
2025-04-25 10:03:56 +08:00
parent 452922724d
commit 5c2a159792
18 changed files with 995 additions and 66 deletions
@@ -9,6 +9,8 @@ import { PasskeyService } from '../services/passkey.service'; // 导入 PasskeyS
import { NotificationService } from '../services/notification.service'; // 导入 NotificationService
import { AuditLogService } from '../services/audit.service'; // 导入 AuditLogService
import { ipBlacklistService } from '../services/ip-blacklist.service'; // 导入 IP 黑名单服务
import { captchaService } from '../services/captcha.service'; // <-- Import CaptchaService
import { settingsService } from '../services/settings.service'; // <-- Import SettingsService for config check
// Remove top-level db instance acquisition
// const db = getDb();
@@ -51,6 +53,36 @@ export const login = async (req: Request, res: Response): Promise<void> => {
}
try {
// --- CAPTCHA Verification Step ---
const captchaConfig = await settingsService.getCaptchaConfig();
if (captchaConfig.enabled) {
const { captchaToken } = req.body;
if (!captchaToken) {
console.log(`[AuthController] 登录尝试失败: CAPTCHA 已启用但未提供令牌 - ${username}`);
// 记录审计日志等(可选,看是否需要区分)
return res.status(400).json({ message: '需要提供 CAPTCHA 令牌。' });
}
try {
const isCaptchaValid = await captchaService.verifyToken(captchaToken);
if (!isCaptchaValid) {
console.log(`[AuthController] 登录尝试失败: CAPTCHA 验证失败 - ${username}`);
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
ipBlacklistService.recordFailedAttempt(clientIp); // Record failed attempt for invalid CAPTCHA
auditLogService.logAction('LOGIN_FAILURE', { username, reason: 'Invalid CAPTCHA token', ip: clientIp });
notificationService.sendNotification('LOGIN_FAILURE', { username, reason: 'Invalid CAPTCHA token', ip: clientIp });
return res.status(401).json({ message: 'CAPTCHA 验证失败。' });
}
console.log(`[AuthController] CAPTCHA 验证成功 - ${username}`);
} catch (captchaError: any) {
console.error(`[AuthController] CAPTCHA 验证过程中出错 (${username}):`, captchaError.message);
// 如果是配置错误或 API 请求失败,返回 500
return res.status(500).json({ message: 'CAPTCHA 验证服务出错,请稍后重试或检查配置。' });
}
} else {
console.log(`[AuthController] CAPTCHA 未启用,跳过验证 - ${username}`);
}
// --- End CAPTCHA Verification ---
const db = await getDbInstance(); // Get DB instance inside the function
// Use the promisified getDb helper
const user = await getDb<User>(db, 'SELECT id, username, hashed_password, two_factor_secret FROM users WHERE username = ?', [username]);
@@ -774,3 +806,35 @@ export const logout = (req: Request, res: Response): void => {
}
});
};
/**
* 获取公共 CAPTCHA 配置 (GET /api/v1/auth/captcha/config)
* 返回给前端用于显示 CAPTCHA 小部件所需的信息 (不含密钥)。
*/
export const getPublicCaptchaConfig = async (req: Request, res: Response): Promise<void> => {
try {
console.log('[AuthController] Received request for public CAPTCHA config.');
const fullConfig = await settingsService.getCaptchaConfig(); // Use settingsService
// *** IMPORTANT: Filter out secret keys before sending to frontend ***
const publicConfig = {
enabled: fullConfig.enabled,
provider: fullConfig.provider,
hcaptchaSiteKey: fullConfig.hcaptchaSiteKey,
recaptchaSiteKey: fullConfig.recaptchaSiteKey,
};
console.log('[AuthController] Sending public CAPTCHA config to client:', publicConfig);
res.status(200).json(publicConfig);
} catch (error: any) {
console.error('[AuthController] 获取公共 CAPTCHA 配置时出错:', error);
// 即使出错,也返回一个“禁用”状态,避免前端出错
res.status(500).json({
enabled: false,
provider: 'none',
hcaptchaSiteKey: '',
recaptchaSiteKey: '',
error: '获取 CAPTCHA 配置失败'
});
}
};
+7 -1
View File
@@ -11,13 +11,19 @@ import {
verifyPasskeyRegistration, // 导入 Passkey 方法
needsSetup, // 导入 needsSetup 控制器
setupAdmin, // 导入 setupAdmin 控制器
logout // *** 新增:导入 logout 控制器 ***
logout, // *** 新增:导入 logout 控制器 ***
getPublicCaptchaConfig // <-- Import public CAPTCHA config controller
} from './auth.controller';
import { isAuthenticated } from './auth.middleware';
import { ipBlacklistCheckMiddleware } from './ipBlacklistCheck.middleware'; // 导入 IP 黑名单检查中间件
const router = Router();
// --- Public CAPTCHA Configuration ---
// GET /api/v1/auth/captcha/config - 获取公共 CAPTCHA 配置 (公开访问)
router.get('/captcha/config', getPublicCaptchaConfig);
// --- Setup Routes (Public) ---
// GET /api/v1/auth/needs-setup - 检查是否需要初始设置 (公开访问)
router.get('/needs-setup', needsSetup);
@@ -1,10 +1,12 @@
// packages/backend/src/repositories/settings.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { SidebarConfig, LayoutNode, PaneName } from '../types/settings.types'; // <-- Import LayoutNode and PaneName
import { CaptchaSettings } from '../types/settings.types'; // <-- Import CaptchaSettings
import * as sqlite3 from 'sqlite3'; // Import sqlite3 for Database type hint
// Define keys for specific settings
const SIDEBAR_CONFIG_KEY = 'sidebarConfig';
const CAPTCHA_CONFIG_KEY = 'captchaConfig'; // <-- Add key for CAPTCHA settings
export interface Setting {
key: string;
@@ -145,6 +147,74 @@ export const setSidebarConfig = async (config: SidebarConfig): Promise<void> =>
}
};
// --- CAPTCHA Settings ---
/**
* 获取 CAPTCHA 配置
* @returns Promise<CaptchaSettings> - 返回解析后的配置或默认值
*/
export const getCaptchaConfig = async (): Promise<CaptchaSettings> => {
const defaultValue: CaptchaSettings = {
enabled: false,
provider: 'none',
hcaptchaSiteKey: '',
hcaptchaSecretKey: '', // Secret keys should ideally not have defaults stored directly here if possible
recaptchaSiteKey: '',
recaptchaSecretKey: '', // Secret keys should ideally not have defaults stored directly here if possible
};
try {
const jsonString = await settingsRepository.getSetting(CAPTCHA_CONFIG_KEY);
if (jsonString) {
try {
const config = JSON.parse(jsonString);
// Basic validation (add more specific checks if needed)
if (config && typeof config.enabled === 'boolean' && typeof config.provider === 'string') {
// Ensure all keys exist, even if undefined/null from older saves
return {
enabled: config.enabled ?? defaultValue.enabled,
provider: config.provider ?? defaultValue.provider,
hcaptchaSiteKey: config.hcaptchaSiteKey ?? defaultValue.hcaptchaSiteKey,
hcaptchaSecretKey: config.hcaptchaSecretKey ?? defaultValue.hcaptchaSecretKey,
recaptchaSiteKey: config.recaptchaSiteKey ?? defaultValue.recaptchaSiteKey,
recaptchaSecretKey: config.recaptchaSecretKey ?? defaultValue.recaptchaSecretKey,
} as CaptchaSettings;
}
console.warn(`[SettingsRepo] Invalid captchaConfig format found in DB: ${jsonString}. Returning default.`);
} catch (parseError) {
console.error(`[SettingsRepo] Failed to parse captchaConfig JSON from DB: ${jsonString}`, parseError);
}
}
} catch (error) {
console.error(`[SettingsRepo] Error fetching captcha config setting (key: ${CAPTCHA_CONFIG_KEY}):`, error);
}
// Return default if not found, invalid, or error occurred
return defaultValue;
};
/**
* 设置 CAPTCHA 配置
* @param config - The CAPTCHA configuration object
*/
export const setCaptchaConfig = async (config: CaptchaSettings): Promise<void> => {
try {
// Basic validation before stringifying
if (!config || typeof config !== 'object' || typeof config.enabled !== 'boolean' || typeof config.provider !== 'string') {
throw new Error('Invalid CAPTCHA config object provided.');
}
// Ensure secret keys are strings, even if empty
config.hcaptchaSecretKey = config.hcaptchaSecretKey || '';
config.recaptchaSecretKey = config.recaptchaSecretKey || '';
config.hcaptchaSiteKey = config.hcaptchaSiteKey || '';
config.recaptchaSiteKey = config.recaptchaSiteKey || '';
const jsonString = JSON.stringify(config);
await settingsRepository.setSetting(CAPTCHA_CONFIG_KEY, jsonString);
} catch (error) {
console.error(`[SettingsRepo] Error setting CAPTCHA config (key: ${CAPTCHA_CONFIG_KEY}):`, error);
throw new Error('Failed to save CAPTCHA configuration.');
}
};
// --- Initialization ---
@@ -200,6 +270,15 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
right: []
};
const defaultCaptchaSettings: CaptchaSettings = {
enabled: false,
provider: 'none',
hcaptchaSiteKey: '',
hcaptchaSecretKey: '',
recaptchaSiteKey: '',
recaptchaSecretKey: '',
};
// --- Define All Default Settings ---
const defaultSettings: Record<string, string> = {
ipWhitelistEnabled: 'false',
@@ -216,6 +295,7 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
dockerDefaultExpand: 'false', // Default Docker expand state
statusMonitorIntervalSeconds: '3', // Default Status Monitor interval
[SIDEBAR_CONFIG_KEY]: JSON.stringify(defaultSidebarPanesStructure), // Use the defined structure
[CAPTCHA_CONFIG_KEY]: JSON.stringify(defaultCaptchaSettings), // Add default CAPTCHA settings
// Add other default settings here
};
const nowSeconds = Math.floor(Date.now() / 1000);
@@ -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
@@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import { settingsService } from '../services/settings.service';
import { AuditLogService } from '../services/audit.service'; // 引入 AuditLogService
import { ipBlacklistService } from '../services/ip-blacklist.service';
import { UpdateSidebarConfigDto } from '../types/settings.types'; // <-- Correct import path
import { UpdateSidebarConfigDto, UpdateCaptchaSettingsDto, CaptchaSettings } from '../types/settings.types'; // <-- Import CAPTCHA types
const auditLogService = new AuditLogService();
@@ -358,6 +358,69 @@ export const settingsController = {
res.status(500).json({ message: '设置侧栏配置失败', error: error.message });
}
}
} // <-- No comma after the last method
}, // <-- Add comma here
// --- CAPTCHA Settings Controller Methods ---
/**
* 获取公共 CAPTCHA 配置 (不含密钥)
*/
async getCaptchaConfig(req: Request, res: Response): Promise<void> {
try {
console.log('[Controller] Received request to get CAPTCHA config.');
const fullConfig = await settingsService.getCaptchaConfig();
// *** IMPORTANT: Filter out secret keys before sending to frontend ***
const publicConfig = {
enabled: fullConfig.enabled,
provider: fullConfig.provider,
hcaptchaSiteKey: fullConfig.hcaptchaSiteKey,
recaptchaSiteKey: fullConfig.recaptchaSiteKey,
};
console.log('[Controller] Sending public CAPTCHA config to client:', publicConfig);
res.json(publicConfig);
} catch (error: any) {
console.error('[Controller] 获取 CAPTCHA 配置时出错:', error);
res.status(500).json({ message: '获取 CAPTCHA 配置失败', error: error.message });
}
},
/**
* 设置 CAPTCHA 配置
*/
async setCaptchaConfig(req: Request, res: Response): Promise<void> {
console.log('[Controller] Received request to set CAPTCHA config.');
try {
const configDto: UpdateCaptchaSettingsDto = req.body;
// Mask secrets immediately if logging the DTO
console.log('[Controller] Request body (DTO, secrets masked):', { ...configDto, hcaptchaSecretKey: '***', recaptchaSecretKey: '***' });
// --- DTO Validation (Basic) ---
if (!configDto || typeof configDto !== 'object') {
console.warn('[Controller] Invalid CAPTCHA config format received (not an object):', configDto);
res.status(400).json({ message: '无效的请求体,应为 JSON 对象' });
return;
}
// More specific validation happens in the service layer
console.log('[Controller] Calling settingsService.setCaptchaConfig...');
await settingsService.setCaptchaConfig(configDto);
console.log('[Controller] settingsService.setCaptchaConfig completed successfully.');
auditLogService.logAction('CAPTCHA_SETTINGS_UPDATED'); // Add audit log
console.log('[Controller] Sending success response.');
res.status(200).json({ message: 'CAPTCHA 配置已成功更新' });
} catch (error: any) {
console.error('[Controller] 设置 CAPTCHA 配置时出错:', error);
// Handle specific validation errors from the service
if (error.message.includes('无效的') || error.message.includes('必须是')) { // Basic check for validation errors
res.status(400).json({ message: `设置 CAPTCHA 配置失败: ${error.message}` });
} else {
res.status(500).json({ message: '设置 CAPTCHA 配置失败', error: error.message });
}
}
} // <-- No comma after the last method
}; // <-- End of settingsController object
@@ -51,3 +51,9 @@ router.put('/sidebar', settingsController.setSidebarConfig);
export default router;
// +++ 新增:CAPTCHA 配置路由 +++
// GET /api/v1/settings/captcha - 获取公共 CAPTCHA 配置 (不含密钥)
router.get('/captcha', settingsController.getCaptchaConfig);
// PUT /api/v1/settings/captcha - 更新 CAPTCHA 配置
router.put('/captcha', settingsController.setCaptchaConfig);
@@ -31,6 +31,7 @@ export type AuditLogActionType =
// Settings
| 'SETTINGS_UPDATED' // General settings update
| 'IP_WHITELIST_UPDATED' // Specific setting update
| 'CAPTCHA_SETTINGS_UPDATED' // Specific setting update for CAPTCHA
// | 'FOCUS_SWITCHER_SEQUENCE_UPDATED' // Removed
// Notifications
+42 -1
View File
@@ -28,4 +28,45 @@ export interface SidebarConfig {
*/
export interface UpdateSidebarConfigDto extends SidebarConfig {} // Simple alias for now, can add validation later
// You can add other settings-related types here if needed
// You can add other settings-related types here if needed
/**
* CAPTCHA 提供商类型
*/
export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'none';
/**
* CAPTCHA 设置接口
*/
export interface CaptchaSettings {
enabled: boolean; // 是否启用 CAPTCHA
provider: CaptchaProvider; // 当前选择的提供商
hcaptchaSiteKey?: string; // hCaptcha 站点密钥 (公开)
hcaptchaSecretKey?: string; // hCaptcha 秘密密钥 (保密) - 后端存储和使用
recaptchaSiteKey?: string; // Google reCAPTCHA v2 站点密钥 (公开)
recaptchaSecretKey?: string; // Google reCAPTCHA v2 秘密密钥 (保密) - 后端存储和使用
}
/**
* 用于更新 CAPTCHA 设置的 DTO
* (可以添加验证规则)
*/
export interface UpdateCaptchaSettingsDto {
enabled?: boolean;
provider?: CaptchaProvider;
hcaptchaSiteKey?: string;
hcaptchaSecretKey?: string;
recaptchaSiteKey?: string;
recaptchaSecretKey?: string;
}
/**
* 完整的应用设置接口 (聚合所有设置类型)
* 注意:这只是一个示例结构,实际可能需要根据 SettingsRepository 的实现调整
*/
export interface AppSettings {
sidebar?: SidebarConfig;
captcha?: CaptchaSettings;
// 可以添加其他设置模块,例如:
// security?: SecuritySettings;
// general?: GeneralSettings;
}