update
This commit is contained in:
@@ -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 配置失败'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user