This commit is contained in:
Baobhan Sith
2025-04-21 00:56:29 +08:00
parent 32d1f89bb7
commit 492d0ee8dd
12 changed files with 1331 additions and 430 deletions
@@ -2,6 +2,7 @@ import { Database } from 'sqlite3';
import * as schemaSql from './schema';
import * as appearanceRepository from '../repositories/appearance.repository';
import * as terminalThemeRepository from '../repositories/terminal-theme.repository';
import * as settingsRepository from '../repositories/settings.repository'; // <-- Import settings repository
import { presetTerminalThemes } from '../config/preset-themes-definition';
import { runDb } from './connection'; // Import runDb for init functions
@@ -16,20 +17,7 @@ export interface TableDefinition {
// --- Initialization Functions ---
/**
* Initializes default settings in the settings table.
*/
const initSettingsTable = async (db: Database): Promise<void> => {
const defaultSettings = [
{ key: 'ipWhitelistEnabled', value: 'false' },
{ key: 'ipWhitelist', value: '' }
];
for (const setting of defaultSettings) {
// Use INSERT OR IGNORE to avoid errors if settings already exist
await runDb(db, "INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", [setting.key, setting.value]);
}
console.log('[DB Init] 默认 settings 初始化检查完成。');
};
// Remove the old initSettingsTable function, as the logic is now in the repository
/**
* Initializes preset terminal themes.
@@ -66,7 +54,7 @@ export const tableDefinitions: TableDefinition[] = [
{
name: 'settings',
sql: schemaSql.createSettingsTableSQL,
init: initSettingsTable
init: settingsRepository.ensureDefaultSettingsExist // <-- Use the function from the repository
},
{ name: 'audit_logs', sql: schemaSql.createAuditLogsTableSQL },
{ name: 'api_keys', sql: schemaSql.createApiKeysTableSQL },
@@ -1,8 +1,10 @@
// packages/backend/src/repositories/settings.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Import new async helpers
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { SidebarConfig } from '../types/settings.types'; // <-- Correct import path
import * as sqlite3 from 'sqlite3'; // Import sqlite3 for Database type hint
// Remove top-level db instance
// const db = getDb();
// Define keys for specific settings
const SIDEBAR_CONFIG_KEY = 'sidebarConfig';
export interface Setting {
key: string;
@@ -93,3 +95,94 @@ export const settingsRepository = {
}
},
};
// --- Specific Setting Getters/Setters ---
/**
* 获取侧栏配置
* @returns Promise<SidebarConfig> - Returns the parsed config or default
*/
export const getSidebarConfig = async (): Promise<SidebarConfig> => {
const defaultValue: SidebarConfig = { left: [], right: [] };
try {
const jsonString = await settingsRepository.getSetting(SIDEBAR_CONFIG_KEY);
if (jsonString) {
try {
const config = JSON.parse(jsonString);
// Basic validation
if (config && Array.isArray(config.left) && Array.isArray(config.right)) {
// TODO: Add deeper validation if needed (e.g., check if items are valid PaneName)
return config as SidebarConfig;
}
console.warn(`[SettingsRepo] Invalid sidebarConfig format found in DB: ${jsonString}. Returning default.`);
} catch (parseError) {
console.error(`[SettingsRepo] Failed to parse sidebarConfig JSON from DB: ${jsonString}`, parseError);
}
}
} catch (error) {
console.error(`[SettingsRepo] Error fetching sidebar config setting (key: ${SIDEBAR_CONFIG_KEY}):`, error);
}
// Return default if not found, invalid, or error occurred
return defaultValue;
};
/**
* 设置侧栏配置
* @param config - The sidebar configuration object
*/
export const setSidebarConfig = async (config: SidebarConfig): Promise<void> => {
try {
// Basic validation before stringifying
if (!config || typeof config !== 'object' || !Array.isArray(config.left) || !Array.isArray(config.right)) {
throw new Error('Invalid sidebar config object provided.');
}
// TODO: Add deeper validation if needed (e.g., check PaneName validity)
const jsonString = JSON.stringify(config);
await settingsRepository.setSetting(SIDEBAR_CONFIG_KEY, jsonString);
} catch (error) {
console.error(`[SettingsRepo] Error setting sidebar config (key: ${SIDEBAR_CONFIG_KEY}):`, error);
throw new Error('Failed to save sidebar configuration.');
}
};
// --- Initialization ---
/**
* Ensures default settings exist in the settings table.
* This function should be called during database initialization.
* @param db - The active database instance
*/
export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<void> => {
const defaultSettings: Record<string, string> = {
language: 'en', // Default language
ipWhitelistEnabled: 'false',
ipWhitelist: '',
maxLoginAttempts: '5',
loginBanDuration: '300', // 5 minutes in seconds
focusSwitcherSequence: JSON.stringify(["quickCommandsSearch", "commandHistorySearch", "fileManagerSearch", "commandInput", "terminalSearch"]), // Default focus sequence
navBarVisible: 'true', // Default nav bar visibility
layoutTree: 'null', // Default layout tree (null initially)
autoCopyOnSelect: 'false', // Default auto copy setting
showPopupFileEditor: 'false', // Default popup editor setting
shareFileEditorTabs: 'true', // Default editor tab sharing
dockerStatusIntervalSeconds: '5', // Default Docker refresh interval
dockerDefaultExpand: 'false', // Default Docker expand state
statusMonitorIntervalSeconds: '3', // Default Status Monitor interval
[SIDEBAR_CONFIG_KEY]: JSON.stringify({ left: [], right: [] }), // Default sidebar config
// Add other default settings here
};
const nowSeconds = Math.floor(Date.now() / 1000);
const sqlInsertOrIgnore = `INSERT OR IGNORE INTO settings (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`;
console.log('[SettingsRepo] Ensuring default settings exist...');
try {
for (const [key, value] of Object.entries(defaultSettings)) {
await runDb(db, sqlInsertOrIgnore, [key, value, nowSeconds, nowSeconds]);
}
console.log('[SettingsRepo] Default settings check complete.');
} catch (err: any) {
console.error(`[SettingsRepo] Error ensuring default settings:`, err.message);
throw new Error(`Failed to ensure default settings: ${err.message}`);
}
};
@@ -1,4 +1,5 @@
import { settingsRepository, Setting } from '../repositories/settings.repository';
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'; // <-- Correct import path
// +++ 定义默认的焦点切换顺序 +++
const DEFAULT_FOCUS_SEQUENCE = ["quickCommandsSearch", "commandHistorySearch", "fileManagerSearch", "commandInput", "terminalSearch"];
@@ -291,5 +292,68 @@ export const settingsService = {
console.error(`[Service] Error calling settingsRepository.setSetting for key ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}:`, error);
throw new Error('Failed to save status monitor interval setting.');
}
} // *** 最后的方法后面不需要逗号 ***
};
}, // *** 确保这里有逗号 ***
// --- Sidebar Config Specific Functions ---
/**
* 获取侧栏配置
* @returns Promise<SidebarConfig>
*/
async getSidebarConfig(): Promise<SidebarConfig> {
console.log('[SettingsService] Getting sidebar config...');
// Directly call the specific repository function
const config = await getSidebarConfigFromRepo();
console.log('[SettingsService] Returning sidebar config:', config);
return config;
},
/**
* 设置侧栏配置
* @param configDto - The sidebar configuration object from DTO
* @returns Promise<void>
*/
async setSidebarConfig(configDto: UpdateSidebarConfigDto): Promise<void> {
console.log('[SettingsService] Setting sidebar config:', configDto);
// --- Validation ---
if (!configDto || typeof configDto !== 'object' || !Array.isArray(configDto.left) || !Array.isArray(configDto.right)) {
throw new Error('无效的侧栏配置格式。必须包含 left 和 right 数组。');
}
// Validate PaneName (using the type imported)
const validPaneNames: Set<PaneName> = new Set([
'connections', 'terminal', 'commandBar', 'fileManager',
'editor', 'statusMonitor', 'commandHistory', 'quickCommands',
'dockerManager'
]);
const validatePaneArray = (arr: any[], side: string) => {
if (!arr.every(item => typeof item === 'string' && validPaneNames.has(item as PaneName))) {
const invalidItems = arr.filter(item => typeof item !== 'string' || !validPaneNames.has(item as PaneName));
throw new Error(`侧栏配置 (${side}) 包含无效的面板名称: ${invalidItems.join(', ')}`);
}
};
validatePaneArray(configDto.left, 'left');
validatePaneArray(configDto.right, 'right');
// Prevent duplicates (optional, uncomment if needed)
// const allPanes = [...configDto.left, ...configDto.right];
// const uniquePanes = new Set(allPanes);
// if (allPanes.length !== uniquePanes.size) {
// throw new Error('侧栏配置中不允许包含重复的面板。');
// }
// Prepare the data in the exact SidebarConfig format expected by the repo
const configToSave: SidebarConfig = {
left: configDto.left,
right: configDto.right,
};
// 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
}; // <-- End of settingsService object definition
@@ -1,9 +1,10 @@
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'; // 引入 IP 黑名单服务
import { ipBlacklistService } from '../services/ip-blacklist.service';
import { UpdateSidebarConfigDto } from '../types/settings.types'; // <-- Correct import path
const auditLogService = new AuditLogService(); // 实例化 AuditLogService
const auditLogService = new AuditLogService();
export const settingsController = {
/**
@@ -291,5 +292,59 @@ export const settingsController = {
console.error('[Controller] 设置终端选中自动复制时出错:', error);
res.status(500).json({ message: '设置终端选中自动复制失败', error: error.message });
}
} // *** 最后的方法后面不需要逗号 ***
};
}, // *** 确保这里有逗号 ***
// --- Sidebar Config Controller Methods ---
/**
* 获取侧栏配置
*/
async getSidebarConfig(req: Request, res: Response): Promise<void> {
try {
console.log('[Controller] Received request to get sidebar config.');
const config = await settingsService.getSidebarConfig();
console.log('[Controller] Sending sidebar config to client:', config);
res.json(config);
} catch (error: any) {
console.error('[Controller] 获取侧栏配置时出错:', error);
res.status(500).json({ message: '获取侧栏配置失败', error: error.message });
}
},
/**
* 设置侧栏配置
*/
async setSidebarConfig(req: Request, res: Response): Promise<void> {
console.log('[Controller] Received request to set sidebar config.');
try {
const configDto: UpdateSidebarConfigDto = req.body;
console.log('[Controller] Request body:', configDto);
// --- DTO Validation (Basic) ---
// More specific validation happens in the service layer
if (!configDto || typeof configDto !== 'object' || !Array.isArray(configDto.left) || !Array.isArray(configDto.right)) {
console.warn('[Controller] Invalid sidebar config format received:', configDto);
res.status(400).json({ message: '无效的请求体,应为包含 left 和 right 数组的 JSON 对象' });
return;
}
console.log('[Controller] Calling settingsService.setSidebarConfig...');
await settingsService.setSidebarConfig(configDto);
console.log('[Controller] settingsService.setSidebarConfig completed successfully.');
// auditLogService.logAction('SIDEBAR_CONFIG_UPDATED'); // Optional: Add audit log
console.log('[Controller] Sending success response.');
res.status(200).json({ message: '侧栏配置已成功更新' });
} catch (error: any) {
console.error('[Controller] 设置侧栏配置时出错:', error);
// Handle specific validation errors from the service
if (error.message.includes('无效的面板名称') || error.message.includes('无效的侧栏配置格式')) {
res.status(400).json({ message: `设置侧栏配置失败: ${error.message}` });
} else {
res.status(500).json({ message: '设置侧栏配置失败', error: error.message });
}
}
} // <-- No comma after the last method
}; // <-- End of settingsController object
@@ -43,4 +43,11 @@ router.get('/auto-copy-on-select', settingsController.getAutoCopyOnSelect);
// PUT /api/v1/settings/auto-copy-on-select - 更新设置
router.put('/auto-copy-on-select', settingsController.setAutoCopyOnSelect);
// +++ 新增:侧栏配置路由 +++
// GET /api/v1/settings/sidebar - 获取侧栏配置
router.get('/sidebar', settingsController.getSidebarConfig);
// PUT /api/v1/settings/sidebar - 更新侧栏配置
router.put('/sidebar', settingsController.setSidebarConfig);
export default router;
@@ -1,5 +1,8 @@
import type { ITheme } from 'xterm';
// 定义所有可用面板的名称 (后端独立定义)
export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory' | 'quickCommands' | 'dockerManager';
/**
* 外观设置数据结构
*/
@@ -0,0 +1,19 @@
// packages/backend/src/types/settings.types.ts
// Define PaneName here as it's logically related to layout/sidebar settings
export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory' | 'quickCommands' | 'dockerManager';
/**
* 侧栏配置数据结构 (Managed by Settings Repository/Service)
*/
export interface SidebarConfig {
left: PaneName[];
right: PaneName[];
}
/**
* 用于更新侧栏配置的 DTO
*/
export interface UpdateSidebarConfigDto extends SidebarConfig {} // Simple alias for now, can add validation later
// You can add other settings-related types here if needed