update
This commit is contained in:
@@ -1,17 +1,12 @@
|
||||
// packages/backend/src/repositories/appearance.repository.ts
|
||||
// Import new async helpers and the instance getter, ensuring getDb is included
|
||||
import { getDbInstance, runDb, getDb, allDb } from '../database/connection';
|
||||
import { AppearanceSettings, UpdateAppearanceDto } from '../types/appearance.types';
|
||||
import { defaultUiTheme } from '../config/default-themes';
|
||||
// Import findThemeById from terminal theme repository for validation
|
||||
import { findThemeById as findTerminalThemeById } from './terminal-theme.repository';
|
||||
import * as sqlite3 from 'sqlite3'; // Import sqlite3 for Database type hint
|
||||
import * as sqlite3 from 'sqlite3';
|
||||
|
||||
const TABLE_NAME = 'appearance_settings';
|
||||
// Remove SETTINGS_ID as the table is key-value based
|
||||
// const SETTINGS_ID = 1;
|
||||
|
||||
// Define the expected row structure from the database (key-value)
|
||||
|
||||
interface DbAppearanceSettingsRow {
|
||||
key: string;
|
||||
value: string;
|
||||
@@ -19,13 +14,13 @@ interface DbAppearanceSettingsRow {
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
// Helper function to map DB rows (key-value pairs) to AppearanceSettings object
|
||||
|
||||
const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): AppearanceSettings => {
|
||||
const settings: Partial<AppearanceSettings> = {};
|
||||
let latestUpdatedAt = 0;
|
||||
|
||||
for (const row of rows) {
|
||||
// Update latestUpdatedAt
|
||||
// 更新 latestUpdatedAt
|
||||
if (row.updated_at > latestUpdatedAt) {
|
||||
latestUpdatedAt = row.updated_at;
|
||||
}
|
||||
@@ -35,7 +30,6 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
|
||||
settings.customUiTheme = row.value;
|
||||
break;
|
||||
case 'activeTerminalThemeId':
|
||||
// Ensure value is parsed as number or null
|
||||
const parsedId = parseInt(row.value, 10);
|
||||
settings.activeTerminalThemeId = isNaN(parsedId) ? null : parsedId;
|
||||
break;
|
||||
@@ -49,19 +43,17 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
|
||||
settings.editorFontSize = parseInt(row.value, 10);
|
||||
break;
|
||||
case 'terminalBackgroundImage':
|
||||
settings.terminalBackgroundImage = row.value || undefined; // Use undefined if empty string
|
||||
settings.terminalBackgroundImage = row.value || undefined;
|
||||
break;
|
||||
case 'pageBackgroundImage':
|
||||
settings.pageBackgroundImage = row.value || undefined; // Use undefined if empty string
|
||||
settings.pageBackgroundImage = row.value || undefined;
|
||||
break;
|
||||
// Add cases for other potential keys if needed
|
||||
}
|
||||
}
|
||||
|
||||
// Merge with defaults for any missing keys and add _id and updatedAt
|
||||
const defaults = getDefaultAppearanceSettings(); // Get defaults
|
||||
const defaults = getDefaultAppearanceSettings();
|
||||
return {
|
||||
_id: 'global_appearance', // Use a fixed string ID for the conceptual global settings
|
||||
_id: 'global_appearance', // 全局外观设置的固定 ID
|
||||
customUiTheme: settings.customUiTheme ?? defaults.customUiTheme,
|
||||
activeTerminalThemeId: settings.activeTerminalThemeId ?? defaults.activeTerminalThemeId,
|
||||
terminalFontFamily: settings.terminalFontFamily ?? defaults.terminalFontFamily,
|
||||
@@ -69,59 +61,60 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
|
||||
editorFontSize: settings.editorFontSize ?? defaults.editorFontSize,
|
||||
terminalBackgroundImage: settings.terminalBackgroundImage ?? defaults.terminalBackgroundImage,
|
||||
pageBackgroundImage: settings.pageBackgroundImage ?? defaults.pageBackgroundImage,
|
||||
updatedAt: latestUpdatedAt || defaults.updatedAt, // Use latest DB timestamp or default
|
||||
updatedAt: latestUpdatedAt || defaults.updatedAt, // 使用最新的更新时间,否则使用默认时间戳
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// 获取默认外观设置 (Simplified, _id is no longer relevant here)
|
||||
// 获取默认外观设置 (已简化, _id 在此不再相关)
|
||||
const getDefaultAppearanceSettings = (): Omit<AppearanceSettings, '_id'> => {
|
||||
return {
|
||||
customUiTheme: JSON.stringify(defaultUiTheme),
|
||||
activeTerminalThemeId: null, // Default should be null initially
|
||||
activeTerminalThemeId: null, // 初始默认应为 null
|
||||
terminalFontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"',
|
||||
terminalFontSize: 14,
|
||||
editorFontSize: 14,
|
||||
terminalBackgroundImage: undefined,
|
||||
pageBackgroundImage: undefined,
|
||||
updatedAt: Date.now(), // Provide a default timestamp
|
||||
updatedAt: Date.now(), // 提供默认时间戳
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Ensures default settings exist in the key-value table.
|
||||
* This function is called during database initialization.
|
||||
* 确保默认设置存在于键值表中。
|
||||
* 此函数在数据库初始化期间调用。
|
||||
* @param db - 活动的数据库实例
|
||||
*/
|
||||
export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<void> => {
|
||||
const defaults = getDefaultAppearanceSettings();
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
const sqlInsertOrIgnore = `INSERT OR IGNORE INTO ${TABLE_NAME} (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`;
|
||||
|
||||
// Define default key-value pairs to ensure existence
|
||||
// 定义默认键值对以确保存在
|
||||
const defaultEntries: Array<{ key: keyof Omit<AppearanceSettings, '_id' | 'updatedAt'>, value: any }> = [
|
||||
{ key: 'customUiTheme', value: defaults.customUiTheme },
|
||||
{ key: 'activeTerminalThemeId', value: null }, // Start with null
|
||||
{ key: 'activeTerminalThemeId', value: null }, // 以 null 开始
|
||||
{ key: 'terminalFontFamily', value: defaults.terminalFontFamily },
|
||||
{ key: 'terminalFontSize', value: defaults.terminalFontSize },
|
||||
{ key: 'editorFontSize', value: defaults.editorFontSize },
|
||||
{ key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // Use empty string for DB
|
||||
{ key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // Use empty string for DB
|
||||
{ key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // 数据库中使用空字符串
|
||||
{ key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // 数据库中使用空字符串
|
||||
];
|
||||
|
||||
try {
|
||||
for (const entry of defaultEntries) {
|
||||
// Convert value to string for DB storage, handle null/undefined
|
||||
// 将值转换为字符串以存储到数据库,处理 null/undefined
|
||||
let dbValue: string;
|
||||
if (entry.value === null || entry.value === undefined) {
|
||||
dbValue = entry.key === 'activeTerminalThemeId' ? 'null' : ''; // Store null specifically for theme ID, empty otherwise
|
||||
dbValue = entry.key === 'activeTerminalThemeId' ? 'null' : ''; // 主题 ID 特殊存储为 'null',其他情况为空字符串
|
||||
} else if (typeof entry.value === 'object') {
|
||||
dbValue = JSON.stringify(entry.value);
|
||||
} else {
|
||||
dbValue = String(entry.value);
|
||||
}
|
||||
|
||||
// Special handling for activeTerminalThemeId: store null as 'null' string or the number as string
|
||||
// 对 activeTerminalThemeId 的特殊处理:将 null 存储为 'null' 字符串,或将数字存储为字符串
|
||||
if (entry.key === 'activeTerminalThemeId') {
|
||||
dbValue = entry.value === null ? 'null' : String(entry.value);
|
||||
}
|
||||
@@ -129,9 +122,9 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
|
||||
|
||||
await runDb(db, sqlInsertOrIgnore, [entry.key, dbValue, nowSeconds, nowSeconds]);
|
||||
}
|
||||
console.log('[AppearanceRepo] 默认外观设置键值对检查完成。');
|
||||
// console.log('[AppearanceRepo] 默认外观设置键值对检查完成。'); // 移除:信息不太关键
|
||||
|
||||
// After ensuring keys exist, try to set the default theme ID if it's currently null
|
||||
// 确保键存在后,如果当前为 null,则尝试设置默认主题 ID
|
||||
await findAndSetDefaultThemeIdIfNull(db);
|
||||
|
||||
} catch (err: any) {
|
||||
@@ -141,64 +134,67 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the default terminal theme ID and updates the 'activeTerminalThemeId' setting if it's currently null.
|
||||
* @param db - The active database instance
|
||||
* 查找默认终端主题 ID,并在 'activeTerminalThemeId' 设置当前为 null 时更新它。
|
||||
* @param db - 活动的数据库实例
|
||||
*/
|
||||
const findAndSetDefaultThemeIdIfNull = async (db: sqlite3.Database): Promise<void> => {
|
||||
try {
|
||||
// Check the current value of activeTerminalThemeId
|
||||
// 检查 activeTerminalThemeId 的当前值
|
||||
const currentSetting = await getDb<{ value: string }>(db, `SELECT value FROM ${TABLE_NAME} WHERE key = ?`, ['activeTerminalThemeId']);
|
||||
|
||||
// Proceed only if the setting exists and its value represents null ('null' string)
|
||||
// 仅当设置存在且其值为 'null' 字符串时继续
|
||||
if (currentSetting && currentSetting.value === 'null') {
|
||||
// Find the default theme from the terminal_themes table (assuming name 'default' marks the default)
|
||||
// 从 terminal_themes 表中查找默认主题(假设名称 'default' 标记为默认)
|
||||
const defaultThemeSql = `SELECT id FROM terminal_themes WHERE name = 'default' AND theme_type = 'preset' LIMIT 1`;
|
||||
const defaultThemeRow = await getDb<{ id: number }>(db, defaultThemeSql);
|
||||
|
||||
if (defaultThemeRow) {
|
||||
const defaultThemeIdNum = defaultThemeRow.id;
|
||||
console.log(`[AppearanceRepo] activeTerminalThemeId 为 null,尝试设置为默认主题 ID: ${defaultThemeIdNum}`);
|
||||
// Update the setting using INSERT OR REPLACE
|
||||
// console.log(`[AppearanceRepo] activeTerminalThemeId 为 null,尝试设置为默认主题 ID: ${defaultThemeIdNum}`); // 移除:信息不太关键
|
||||
// 使用 INSERT OR REPLACE 更新设置
|
||||
const sqlReplace = `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, updated_at) VALUES (?, ?, ?)`;
|
||||
await runDb(db, sqlReplace, ['activeTerminalThemeId', String(defaultThemeIdNum), Math.floor(Date.now() / 1000)]);
|
||||
} else {
|
||||
console.warn("[AppearanceRepo] 未找到名为 'default' 的预设终端主题,无法设置默认 activeTerminalThemeId。");
|
||||
}
|
||||
} else {
|
||||
// console.log(`[AppearanceRepo] activeTerminalThemeId 已设置 (${currentSetting?.value}) 或键不存在,跳过设置默认 ID。`);
|
||||
}
|
||||
// 如果 activeTerminalThemeId 已设置或键不存在,则不执行任何操作
|
||||
} catch (error: any) {
|
||||
console.error("[AppearanceRepo] 设置默认终端主题 ID 时出错:", error.message);
|
||||
// Don't throw here, just log
|
||||
// 这里不抛出错误,只记录日志
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 获取外观设置
|
||||
* @returns Promise<AppearanceSettings>
|
||||
* 获取外观设置。
|
||||
* 从数据库中检索所有外观相关的键值对,并将它们映射到一个 AppearanceSettings 对象。
|
||||
* @returns {Promise<AppearanceSettings>} 返回包含当前外观设置的对象。
|
||||
* @throws {Error} 如果从数据库获取设置失败。
|
||||
*/
|
||||
export const getAppearanceSettings = async (): Promise<AppearanceSettings> => {
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
// Fetch all rows from the key-value table
|
||||
// 从键值表中获取所有行
|
||||
const rows = await allDb<DbAppearanceSettingsRow>(db, `SELECT key, value, updated_at FROM ${TABLE_NAME}`);
|
||||
return mapRowsToAppearanceSettings(rows); // Map the key-value pairs to the settings object
|
||||
return mapRowsToAppearanceSettings(rows); // 将键值对映射到设置对象
|
||||
} catch (err: any) {
|
||||
console.error('获取外观设置失败:', err.message);
|
||||
console.error('[AppearanceRepo] 获取外观设置失败:', err.message);
|
||||
throw new Error('获取外观设置失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新外观设置 (Public API)
|
||||
* @param settingsDto 更新的数据
|
||||
* @returns Promise<boolean> 是否成功更新
|
||||
* 更新外观设置 (公共 API)。
|
||||
* 接收一个包含要更新设置的 DTO,执行必要的验证,然后调用内部更新函数。
|
||||
* @param {UpdateAppearanceDto} settingsDto - 包含要更新设置的对象。
|
||||
* @returns {Promise<boolean>} 如果至少有一个设置被成功更新或插入,则返回 true,否则返回 false。
|
||||
* @throws {Error} 如果验证失败或内部更新过程中发生错误。
|
||||
*/
|
||||
export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto): Promise<boolean> => {
|
||||
const db = await getDbInstance();
|
||||
// Perform validation or complex logic if needed before calling internal update
|
||||
// Example validation (already present in service, but could be here too):
|
||||
// 在调用内部更新之前,如果需要,执行验证或复杂逻辑
|
||||
// 验证示例(已存在于服务中,但也可以在这里):
|
||||
if (settingsDto.activeTerminalThemeId !== undefined && settingsDto.activeTerminalThemeId !== null) {
|
||||
try {
|
||||
const themeExists = await findTerminalThemeById(settingsDto.activeTerminalThemeId);
|
||||
@@ -210,18 +206,20 @@ export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto)
|
||||
throw new Error(`验证主题 ID 失败: ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
// ... other validations ...
|
||||
// ... 其他验证 ...
|
||||
|
||||
return updateAppearanceSettingsInternal(db, settingsDto);
|
||||
};
|
||||
|
||||
/**
|
||||
* 内部更新外观设置函数 (供内部调用,如初始化)
|
||||
* @param db - Active database instance
|
||||
* @param settingsDto - Data to update
|
||||
* @returns Promise<boolean> - Success status
|
||||
* 内部更新外观设置函数 (供内部调用,例如在初始化或公共 API 中)。
|
||||
* 此函数直接与数据库交互,使用 INSERT OR REPLACE 来更新或插入键值对。
|
||||
* @param {sqlite3.Database} db - 活动的数据库实例。
|
||||
* @param {UpdateAppearanceDto} settingsDto - 包含要更新设置的对象。
|
||||
* @returns {Promise<boolean>} 如果至少有一个设置被成功更新或插入,则返回 true,否则返回 false。
|
||||
* @throws {Error} 如果在数据库操作期间发生错误。
|
||||
*/
|
||||
// Internal function to update settings in the key-value table
|
||||
// 在键值表中更新设置的内部函数
|
||||
const updateAppearanceSettingsInternal = async (db: sqlite3.Database, settingsDto: UpdateAppearanceDto): Promise<boolean> => {
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
const sqlReplace = `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, updated_at) VALUES (?, ?, ?)`;
|
||||
@@ -232,37 +230,36 @@ const updateAppearanceSettingsInternal = async (db: sqlite3.Database, settingsDt
|
||||
const value = settingsDto[key];
|
||||
let dbValue: string;
|
||||
|
||||
// Convert value to string for DB, handle null/undefined
|
||||
// 将值转换为字符串以存储到数据库,处理 null/undefined
|
||||
if (value === null || value === undefined) {
|
||||
dbValue = key === 'activeTerminalThemeId' ? 'null' : ''; // Store null specifically for theme ID
|
||||
dbValue = key === 'activeTerminalThemeId' ? 'null' : ''; // 主题 ID 特殊存储为 'null'
|
||||
} else if (typeof value === 'object') {
|
||||
dbValue = JSON.stringify(value);
|
||||
} else {
|
||||
dbValue = String(value);
|
||||
}
|
||||
|
||||
// Special handling for activeTerminalThemeId to store 'null' string or number string
|
||||
// 对 activeTerminalThemeId 的特殊处理:存储 'null' 字符串或数字字符串
|
||||
if (key === 'activeTerminalThemeId') {
|
||||
dbValue = value === null ? 'null' : String(value);
|
||||
}
|
||||
|
||||
|
||||
// Validation for active_terminal_theme_id type before saving
|
||||
// 保存前验证 active_terminal_theme_id 类型
|
||||
if (key === 'activeTerminalThemeId' && value !== null && typeof value !== 'number') {
|
||||
console.error(`[AppearanceRepo] 更新 activeTerminalThemeId 时收到无效类型值: ${value} (类型: ${typeof value}),应为数字或 null。跳过此字段。`);
|
||||
continue; // Skip this key
|
||||
continue; // 跳过此键
|
||||
}
|
||||
|
||||
// Execute INSERT OR REPLACE for each key-value pair
|
||||
// 对每个键值对执行 INSERT OR REPLACE
|
||||
const result = await runDb(db, sqlReplace, [key, dbValue, nowSeconds]);
|
||||
if (result.changes > 0) {
|
||||
changesMade = true;
|
||||
}
|
||||
}
|
||||
console.log(`[AppearanceRepo] 更新外观设置完成。是否有更改: ${changesMade}`);
|
||||
return changesMade; // Return true if any row was inserted or replaced
|
||||
return changesMade; // 如果有任何行被插入或替换,则返回 true
|
||||
} catch (err: any) {
|
||||
console.error('更新外观设置失败:', err.message);
|
||||
console.error('[AppearanceRepo] 更新外观设置失败:', err.message);
|
||||
throw new Error('更新外观设置失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
// packages/backend/src/repositories/audit.repository.ts
|
||||
import { Database } from 'sqlite3';
|
||||
// Import new async helpers and the instance getter
|
||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||
import { AuditLogEntry, AuditLogActionType } from '../types/audit.types';
|
||||
|
||||
// Define the expected row structure from the database if it matches AuditLogEntry
|
||||
|
||||
type DbAuditLogRow = AuditLogEntry;
|
||||
|
||||
export class AuditLogRepository {
|
||||
// Remove constructor or leave it empty
|
||||
// constructor() { }
|
||||
|
||||
|
||||
/**
|
||||
* 添加一条审计日志记录
|
||||
* @param actionType 操作类型
|
||||
* @param details 可选的详细信息 (对象或字符串)
|
||||
* 添加一条审计日志记录。
|
||||
* @param actionType 操作类型。
|
||||
* @param details 可选的详细信息(对象或字符串)。
|
||||
*/
|
||||
async addLog(actionType: AuditLogActionType, details?: Record<string, any> | string | null): Promise<void> {
|
||||
const timestamp = Math.floor(Date.now() / 1000); // Unix timestamp in seconds
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
let detailsString: string | null = null;
|
||||
|
||||
if (details) {
|
||||
try {
|
||||
detailsString = typeof details === 'string' ? details : JSON.stringify(details);
|
||||
} catch (error: any) {
|
||||
console.error(`[Audit Log] Failed to stringify details for action ${actionType}:`, error.message);
|
||||
console.error(`[审计日志] 序列化操作 ${actionType} 的详情失败:`, error.message);
|
||||
detailsString = JSON.stringify({ error: 'Failed to stringify details', originalDetails: String(details) }); // Ensure originalDetails is stringifiable
|
||||
}
|
||||
}
|
||||
@@ -35,22 +32,20 @@ export class AuditLogRepository {
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
await runDb(db, sql, params);
|
||||
// console.log(`[Audit Log] Logged action: ${actionType}`); // Optional: verbose logging
|
||||
|
||||
// --- 添加日志清理逻辑 ---
|
||||
await this.cleanupOldLogs(db);
|
||||
// --- 清理逻辑结束 ---
|
||||
|
||||
} catch (err: any) {
|
||||
console.error(`[Audit Log] Error adding log entry for action ${actionType}: ${err.message}`);
|
||||
// Decide if logging failure should throw an error or just be logged
|
||||
// throw new Error(`Error adding log entry: ${err.message}`); // Uncomment to make it critical
|
||||
console.error(`[审计日志] 添加操作 ${actionType} 的日志条目时出错: ${err.message}`);
|
||||
// 决定日志记录失败是应该抛出错误还是仅记录日志
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧的审计日志,保持最多 MAX_LOG_ENTRIES 条记录
|
||||
* @param db - 数据库实例
|
||||
* 清理旧的审计日志,保持最多 MAX_LOG_ENTRIES 条记录。
|
||||
* @param db - 数据库实例。
|
||||
*/
|
||||
private async cleanupOldLogs(db: Database): Promise<void> {
|
||||
const MAX_LOG_ENTRIES = 50000; // 设置最大日志条数
|
||||
@@ -71,24 +66,23 @@ export class AuditLogRepository {
|
||||
|
||||
if (total > MAX_LOG_ENTRIES) {
|
||||
const logsToDelete = total - MAX_LOG_ENTRIES;
|
||||
console.log(`[Audit Log] Log count (${total}) exceeds limit (${MAX_LOG_ENTRIES}). Deleting ${logsToDelete} oldest entries.`);
|
||||
console.log(`[审计日志] 日志数量 (${total}) 超过限制 (${MAX_LOG_ENTRIES})。正在删除 ${logsToDelete} 条最旧的记录。`);
|
||||
await runDb(db, deleteSql, [logsToDelete]);
|
||||
console.log(`[Audit Log] Successfully deleted ${logsToDelete} oldest log entries.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`[Audit Log] Error during log cleanup: ${err.message}`);
|
||||
// 清理失败不应阻止主日志记录流程,仅记录错误
|
||||
console.error(`[审计日志] 日志清理过程中出错: ${err.message}`);
|
||||
// 清理失败不应阻止主日志记录流程,仅记录错误。
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审计日志列表 (支持分页和基本过滤)
|
||||
* @param limit 每页数量
|
||||
* @param offset 偏移量
|
||||
* @param actionType 可选的操作类型过滤
|
||||
* @param startDate 可选的开始时间戳 (秒)
|
||||
* @param endDate 可选的结束时间戳 (秒)
|
||||
* @param searchTerm 可选的搜索关键词 (模糊匹配 details)
|
||||
* 获取审计日志列表(支持分页和基本过滤)。
|
||||
* @param limit 每页数量。
|
||||
* @param offset 偏移量。
|
||||
* @param actionType 可选的操作类型过滤。
|
||||
* @param startDate 可选的开始时间戳(秒)。
|
||||
* @param endDate 可选的结束时间戳(秒)。
|
||||
* @param searchTerm 可选的搜索关键词(模糊匹配 details)。
|
||||
*/
|
||||
async getLogs(
|
||||
limit: number = 50,
|
||||
@@ -98,8 +92,7 @@ export class AuditLogRepository {
|
||||
endDate?: number,
|
||||
searchTerm?: string // 添加 searchTerm 参数
|
||||
): Promise<{ logs: AuditLogEntry[], total: number }> {
|
||||
console.log(`[Audit Repo] getLogs called with: actionType=${actionType}, searchTerm=${searchTerm}`); // 添加日志
|
||||
|
||||
|
||||
let baseSql = 'SELECT * FROM audit_logs';
|
||||
let countSql = 'SELECT COUNT(*) as total FROM audit_logs';
|
||||
const whereClauses: string[] = [];
|
||||
@@ -107,14 +100,12 @@ export class AuditLogRepository {
|
||||
const countParams: (string | number)[] = [];
|
||||
|
||||
if (actionType) {
|
||||
console.log(`[Audit Repo] Filtering by actionType: ${actionType}`); // 添加日志
|
||||
whereClauses.push('action_type = ?');
|
||||
params.push(actionType);
|
||||
countParams.push(actionType);
|
||||
}
|
||||
// 添加 searchTerm 的过滤逻辑
|
||||
if (searchTerm) {
|
||||
console.log(`[Audit Repo] Filtering by searchTerm: ${searchTerm}`); // 添加日志
|
||||
// 搜索 details 字段,使用 LIKE 进行模糊匹配
|
||||
whereClauses.push('details LIKE ?');
|
||||
const searchTermLike = `%${searchTerm}%`;
|
||||
@@ -132,25 +123,20 @@ export class AuditLogRepository {
|
||||
baseSql += ' ORDER BY timestamp DESC LIMIT ? OFFSET ?';
|
||||
params.push(limit, offset);
|
||||
|
||||
console.log(`[Audit Repo] Executing count SQL: ${countSql} with params:`, countParams); // 添加日志
|
||||
console.log(`[Audit Repo] Executing base SQL: ${baseSql} with params:`, params); // 添加日志
|
||||
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
// First get the total count
|
||||
const countRow = await getDbRow<{ total: number }>(db, countSql, countParams);
|
||||
const total = countRow?.total ?? 0;
|
||||
|
||||
// Then get the paginated logs
|
||||
const logs = await allDb<DbAuditLogRow>(db, baseSql, params);
|
||||
|
||||
return { logs, total };
|
||||
} catch (err: any) {
|
||||
console.error(`Error fetching audit logs:`, err.message);
|
||||
throw new Error(`Error fetching audit logs: ${err.message}`);
|
||||
console.error(`获取审计日志时出错:`, err.message);
|
||||
throw new Error(`获取审计日志时出错: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export the class (Removed redundant export below as class is already exported)
|
||||
// export { AuditLogRepository };
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// packages/backend/src/repositories/command-history.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';
|
||||
|
||||
// 定义命令历史记录的接口
|
||||
export interface CommandHistoryEntry {
|
||||
@@ -8,7 +7,6 @@ export interface CommandHistoryEntry {
|
||||
timestamp: number; // Unix 时间戳 (秒)
|
||||
}
|
||||
|
||||
// Define the expected row structure from the database if it matches CommandHistoryEntry
|
||||
type DbCommandHistoryRow = CommandHistoryEntry;
|
||||
|
||||
/**
|
||||
@@ -94,7 +92,7 @@ export const clearAllCommands = async (): Promise<number> => {
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql);
|
||||
return result.changes; // Return the number of deleted rows
|
||||
return result.changes;
|
||||
} catch (err: any) {
|
||||
console.error('清空命令历史记录时出错:', err.message);
|
||||
throw new Error('无法清空命令历史记录');
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
// packages/backend/src/repositories/connection.repository.ts
|
||||
import { Database, Statement } from 'sqlite3';
|
||||
// Import new async helpers and the instance getter
|
||||
import { Database } from 'sqlite3';
|
||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||
|
||||
// Remove top-level db instance
|
||||
// const db = getDb();
|
||||
|
||||
// Define Connection 类型 (可以从 controller 或 types 文件导入,暂时在此定义)
|
||||
// 注意:这里不包含加密字段,因为 Repository 不应处理解密
|
||||
interface ConnectionBase {
|
||||
id: number;
|
||||
name: string | null; // 允许 name 为 null
|
||||
name: string | null;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
@@ -21,9 +17,8 @@ interface ConnectionBase {
|
||||
last_connected_at: number | null;
|
||||
}
|
||||
|
||||
// Type for the result of the JOIN query in findAllConnectionsWithTags and findConnectionByIdWithTags
|
||||
interface ConnectionWithTagsRow extends ConnectionBase {
|
||||
tag_ids_str: string | null; // Raw string from GROUP_CONCAT
|
||||
tag_ids_str: string | null;
|
||||
}
|
||||
|
||||
export interface ConnectionWithTags extends ConnectionBase {
|
||||
@@ -35,12 +30,10 @@ export interface FullConnectionData extends ConnectionBase {
|
||||
encrypted_password?: string | null;
|
||||
encrypted_private_key?: string | null;
|
||||
encrypted_passphrase?: string | null;
|
||||
// Include tag_ids for creation/update convenience if needed, handled separately
|
||||
tag_ids?: number[];
|
||||
}
|
||||
|
||||
// Type for the result of the JOIN query in findFullConnectionById
|
||||
// Define a more specific type for the complex row structure
|
||||
|
||||
interface FullConnectionDbRow extends FullConnectionData {
|
||||
proxy_db_id: number | null;
|
||||
proxy_name: string | null;
|
||||
@@ -70,7 +63,6 @@ export const findAllConnectionsWithTags = async (): Promise<ConnectionWithTags[]
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const rows = await allDb<ConnectionWithTagsRow>(db, sql);
|
||||
// Safely map rows, handling potential null tag_ids_str
|
||||
return rows.map(row => ({
|
||||
...row,
|
||||
tag_ids: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
|
||||
@@ -97,7 +89,7 @@ export const findConnectionByIdWithTags = async (id: number): Promise<Connection
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const row = await getDbRow<ConnectionWithTagsRow>(db, sql, [id]);
|
||||
if (row && typeof row.id !== 'undefined') { // Check if a valid row was found
|
||||
if (row && typeof row.id !== 'undefined') {
|
||||
return {
|
||||
...row,
|
||||
tag_ids: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
|
||||
@@ -155,7 +147,6 @@ export const createConnection = async (data: Omit<FullConnectionData, 'id' | 'cr
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, params);
|
||||
// Ensure lastID is valid before returning
|
||||
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
|
||||
throw new Error('创建连接后未能获取有效的 lastID');
|
||||
}
|
||||
@@ -176,7 +167,7 @@ export const updateConnection = async (id: number, data: Partial<Omit<FullConnec
|
||||
delete fieldsToUpdate.id;
|
||||
delete fieldsToUpdate.created_at;
|
||||
delete fieldsToUpdate.last_connected_at;
|
||||
delete fieldsToUpdate.tag_ids; // Tags handled separately
|
||||
delete fieldsToUpdate.tag_ids;
|
||||
|
||||
fieldsToUpdate.updated_at = Math.floor(Date.now() / 1000);
|
||||
|
||||
@@ -209,7 +200,6 @@ export const deleteConnection = async (id: number): Promise<boolean> => {
|
||||
const sql = `DELETE FROM connections WHERE id = ?`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
// ON DELETE CASCADE in connection_tags and ON DELETE SET NULL for proxy_id handle related data
|
||||
const result = await runDb(db, sql, [id]);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
@@ -245,21 +235,18 @@ export const updateLastConnected = async (id: number, timestamp: number): Promis
|
||||
*/
|
||||
export const updateConnectionTags = async (connectionId: number, tagIds: number[]): Promise<void> => {
|
||||
const db = await getDbInstance();
|
||||
// Use a transaction to ensure atomicity
|
||||
try {
|
||||
await runDb(db, 'BEGIN TRANSACTION');
|
||||
|
||||
// 1. Delete old associations
|
||||
|
||||
await runDb(db, `DELETE FROM connection_tags WHERE connection_id = ?`, [connectionId]);
|
||||
|
||||
// 2. Insert new associations (if any)
|
||||
if (tagIds.length > 0) {
|
||||
const insertSql = `INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`;
|
||||
// Use Promise.all for potentially better performance, though sequential inserts are safer for constraints
|
||||
|
||||
const insertPromises = tagIds
|
||||
.filter(tagId => typeof tagId === 'number' && tagId > 0) // Basic validation
|
||||
.filter(tagId => typeof tagId === 'number' && tagId > 0)
|
||||
.map(tagId => runDb(db, insertSql, [connectionId, tagId]).catch(err => {
|
||||
// Log warning but don't fail the whole transaction for a single tag insert error (e.g., invalid tag ID)
|
||||
console.warn(`Repository: 更新连接 ${connectionId} 标签时,插入 tag_id ${tagId} 失败: ${err.message}`);
|
||||
}));
|
||||
await Promise.all(insertPromises);
|
||||
@@ -269,21 +256,20 @@ export const updateConnectionTags = async (connectionId: number, tagIds: number[
|
||||
} catch (err: any) {
|
||||
console.error(`Repository: 更新连接 ${connectionId} 的标签关联时出错:`, err.message);
|
||||
try {
|
||||
await runDb(db, 'ROLLBACK'); // Attempt to rollback on error
|
||||
await runDb(db, 'ROLLBACK');
|
||||
} catch (rollbackErr: any) {
|
||||
console.error(`Repository: 回滚连接 ${connectionId} 的标签更新事务失败:`, rollbackErr.message);
|
||||
}
|
||||
throw new Error('处理标签关联失败'); // Re-throw original error
|
||||
throw new Error('处理标签关联失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量插入连接(用于导入)
|
||||
* 注意:此函数应在事务中调用 (由调用者负责事务)
|
||||
* Returns an array mapping new connection IDs to their original import data (for tag association)
|
||||
*/
|
||||
export const bulkInsertConnections = async (
|
||||
db: Database, // Pass the transaction-aware db instance
|
||||
db: Database,
|
||||
connections: Array<Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { tag_ids?: number[] }>
|
||||
): Promise<{ connectionId: number, originalData: any }[]> => {
|
||||
|
||||
@@ -291,8 +277,6 @@ export const bulkInsertConnections = async (
|
||||
const results: { connectionId: number, originalData: any }[] = [];
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Prepare statement outside the loop for efficiency (though sqlite3 might cache implicitly)
|
||||
// Using direct runDb might be simpler here unless performance is critical
|
||||
|
||||
for (const connData of connections) {
|
||||
const params = [
|
||||
@@ -304,19 +288,15 @@ export const bulkInsertConnections = async (
|
||||
now, now
|
||||
];
|
||||
try {
|
||||
// Use the passed db instance (which should be in a transaction)
|
||||
const connResult = await runDb(db, insertConnSql, params);
|
||||
if (typeof connResult.lastID !== 'number' || connResult.lastID <= 0) {
|
||||
throw new Error(`插入连接 "${connData.name}" 后未能获取有效的 lastID`);
|
||||
}
|
||||
results.push({ connectionId: connResult.lastID, originalData: connData });
|
||||
} catch (err: any) {
|
||||
// Log error but continue with other connections? Or re-throw to fail the whole batch?
|
||||
console.error(`Repository: 批量插入连接 "${connData.name}" 时出错: ${err.message}`);
|
||||
// Decide on error handling strategy for batch operations
|
||||
throw new Error(`批量插入连接 "${connData.name}" 失败`); // Fail fast for now
|
||||
throw new Error(`批量插入连接 "${connData.name}" 失败`);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
// Tag insertion should be handled separately after connections are inserted, using the returned IDs
|
||||
};
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// packages/backend/src/repositories/notification.repository.ts
|
||||
import { Database } from 'sqlite3';
|
||||
// Import new async helpers and the instance getter
|
||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||
import { NotificationSetting, RawNotificationSetting, NotificationChannelType, NotificationEvent, NotificationChannelConfig } from '../types/notification.types';
|
||||
|
||||
// Helper to parse raw data from DB
|
||||
|
||||
const parseRawSetting = (raw: RawNotificationSetting): NotificationSetting => {
|
||||
try {
|
||||
return {
|
||||
@@ -13,20 +10,18 @@ const parseRawSetting = (raw: RawNotificationSetting): NotificationSetting => {
|
||||
config: JSON.parse(raw.config || '{}'),
|
||||
enabled_events: JSON.parse(raw.enabled_events || '[]'),
|
||||
};
|
||||
} catch (error: any) { // Add type annotation
|
||||
console.error(`Error parsing notification setting ID ${raw.id}:`, error.message);
|
||||
} catch (error: any) {
|
||||
console.error(`解析通知设置 ID ${raw.id} 时出错:`, error.message);
|
||||
return {
|
||||
...raw,
|
||||
enabled: Boolean(raw.enabled),
|
||||
config: {} as NotificationChannelConfig, // Indicate parsing error
|
||||
config: {} as NotificationChannelConfig,
|
||||
enabled_events: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export class NotificationSettingsRepository {
|
||||
// Remove constructor or leave it empty
|
||||
// constructor() { }
|
||||
|
||||
async getAll(): Promise<NotificationSetting[]> {
|
||||
try {
|
||||
@@ -34,8 +29,8 @@ export class NotificationSettingsRepository {
|
||||
const rows = await allDb<RawNotificationSetting>(db, 'SELECT * FROM notification_settings ORDER BY created_at ASC');
|
||||
return rows.map(parseRawSetting);
|
||||
} catch (err: any) {
|
||||
console.error(`Error fetching notification settings:`, err.message);
|
||||
throw new Error(`Error fetching notification settings: ${err.message}`);
|
||||
console.error(`获取通知设置时出错:`, err.message);
|
||||
throw new Error(`获取通知设置时出错: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,13 +40,12 @@ export class NotificationSettingsRepository {
|
||||
const row = await getDbRow<RawNotificationSetting>(db, 'SELECT * FROM notification_settings WHERE id = ?', [id]);
|
||||
return row ? parseRawSetting(row) : null;
|
||||
} catch (err: any) {
|
||||
console.error(`Error fetching notification setting by ID ${id}:`, err.message);
|
||||
throw new Error(`Error fetching notification setting by ID ${id}: ${err.message}`);
|
||||
console.error(`通过 ID ${id} 获取通知设置时出错:`, err.message);
|
||||
throw new Error(`通过 ID ${id} 获取通知设置时出错: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getEnabledByEvent(event: NotificationEvent): Promise<NotificationSetting[]> {
|
||||
// Note: Query remains inefficient, consider optimization later if needed.
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const rows = await allDb<RawNotificationSetting>(db, 'SELECT * FROM notification_settings WHERE enabled = 1');
|
||||
@@ -59,8 +53,8 @@ export class NotificationSettingsRepository {
|
||||
const filteredRows = parsedRows.filter(setting => setting.enabled_events.includes(event));
|
||||
return filteredRows;
|
||||
} catch (err: any) {
|
||||
console.error(`Error fetching enabled notification settings:`, err.message);
|
||||
throw new Error(`Error fetching enabled notification settings: ${err.message}`);
|
||||
console.error(`获取启用的通知设置时出错:`, err.message);
|
||||
throw new Error(`获取启用的通知设置时出错: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +62,10 @@ export class NotificationSettingsRepository {
|
||||
const sql = `
|
||||
INSERT INTO notification_settings (channel_type, name, enabled, config, enabled_events, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
|
||||
`; // Added created_at, updated_at
|
||||
`;
|
||||
const params = [
|
||||
setting.channel_type,
|
||||
setting.name ?? '', // Ensure name is not undefined
|
||||
setting.name ?? '',
|
||||
setting.enabled ? 1 : 0,
|
||||
JSON.stringify(setting.config || {}),
|
||||
JSON.stringify(setting.enabled_events || [])
|
||||
@@ -85,17 +79,15 @@ export class NotificationSettingsRepository {
|
||||
}
|
||||
return result.lastID;
|
||||
} catch (err: any) {
|
||||
console.error(`Error creating notification setting:`, err.message);
|
||||
throw new Error(`Error creating notification setting: ${err.message}`);
|
||||
console.error(`创建通知设置时出错:`, err.message);
|
||||
throw new Error(`创建通知设置时出错: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: number, setting: Partial<Omit<NotificationSetting, 'id' | 'created_at' | 'updated_at'>>): Promise<boolean> {
|
||||
// Build the SET part of the query dynamically
|
||||
const fields: string[] = [];
|
||||
const params: (string | number | null)[] = [];
|
||||
|
||||
// Dynamically build SET clauses
|
||||
if (setting.channel_type !== undefined) { fields.push('channel_type = ?'); params.push(setting.channel_type); }
|
||||
if (setting.name !== undefined) { fields.push('name = ?'); params.push(setting.name); }
|
||||
if (setting.enabled !== undefined) { fields.push('enabled = ?'); params.push(setting.enabled ? 1 : 0); }
|
||||
@@ -103,11 +95,11 @@ export class NotificationSettingsRepository {
|
||||
if (setting.enabled_events !== undefined) { fields.push('enabled_events = ?'); params.push(JSON.stringify(setting.enabled_events || [])); }
|
||||
|
||||
if (fields.length === 0) {
|
||||
console.warn(`[NotificationRepo] update called for ID ${id} with no fields to update.`);
|
||||
return true; // Or false, depending on desired behavior for no-op update
|
||||
console.warn(`[通知仓库] 针对 ID ${id} 调用了更新,但没有要更新的字段。`);
|
||||
return true;
|
||||
}
|
||||
|
||||
fields.push('updated_at = strftime(\'%s\', \'now\')'); // Always update timestamp
|
||||
fields.push('updated_at = strftime(\'%s\', \'now\')');
|
||||
|
||||
const sql = `UPDATE notification_settings SET ${fields.join(', ')} WHERE id = ?`;
|
||||
params.push(id);
|
||||
@@ -117,8 +109,8 @@ export class NotificationSettingsRepository {
|
||||
const result = await runDb(db, sql, params);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error(`Error updating notification setting ID ${id}:`, err.message);
|
||||
throw new Error(`Error updating notification setting ID ${id}: ${err.message}`);
|
||||
console.error(`更新通知设置 ID ${id} 时出错:`, err.message);
|
||||
throw new Error(`更新通知设置 ID ${id} 时出错: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,11 +121,9 @@ export class NotificationSettingsRepository {
|
||||
const result = await runDb(db, sql, [id]);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error(`Error deleting notification setting ID ${id}:`, err.message);
|
||||
throw new Error(`Error deleting notification setting ID ${id}: ${err.message}`);
|
||||
console.error(`删除通知设置 ID ${id} 时出错:`, err.message);
|
||||
throw new Error(`删除通知设置 ID ${id} 时出错: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export the class (Removed redundant export below as class is already exported)
|
||||
// export { NotificationSettingsRepository };
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// packages/backend/src/repositories/passkey.repository.ts
|
||||
import { Database } from 'sqlite3';
|
||||
// Import new async helpers and the instance getter
|
||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||
|
||||
// 定义 Passkey 数据库记录的接口
|
||||
@@ -9,18 +6,16 @@ export interface PasskeyRecord {
|
||||
credential_id: string; // Base64URL encoded
|
||||
public_key: string; // Base64URL encoded
|
||||
counter: number;
|
||||
transports: string | null; // JSON string or null
|
||||
transports: string | null;
|
||||
name: string | null;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
// Define the expected row structure from the database if it matches PasskeyRecord
|
||||
|
||||
type DbPasskeyRow = PasskeyRecord;
|
||||
|
||||
export class PasskeyRepository {
|
||||
// Remove constructor or leave it empty, db instance will be fetched in each method
|
||||
// constructor() { }
|
||||
|
||||
/**
|
||||
* 保存新的 Passkey 凭证
|
||||
@@ -48,7 +43,6 @@ export class PasskeyRepository {
|
||||
return result.lastID;
|
||||
} catch (err: any) {
|
||||
console.error('保存 Passkey 时出错:', err.message);
|
||||
// Handle potential UNIQUE constraint errors on credential_id
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
throw new Error(`Credential ID "${credentialId}" 已存在。`);
|
||||
}
|
||||
@@ -76,12 +70,10 @@ export class PasskeyRepository {
|
||||
* 获取所有已注册的 Passkey 记录 (仅选择必要字段)
|
||||
* @returns Promise<Partial<PasskeyRecord>[]> 所有记录的部分信息的数组
|
||||
*/
|
||||
// Adjust return type based on selected columns
|
||||
async getAllPasskeys(): Promise<Array<Pick<PasskeyRecord, 'id' | 'credential_id' | 'name' | 'transports' | 'created_at'>>> {
|
||||
const sql = `SELECT id, credential_id, name, transports, created_at FROM passkeys ORDER BY created_at DESC`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
// Adjust the generic type for allDb to match the selected columns
|
||||
const rows = await allDb<Pick<PasskeyRecord, 'id' | 'credential_id' | 'name' | 'transports' | 'created_at'>>(db, sql);
|
||||
return rows;
|
||||
} catch (err: any) {
|
||||
@@ -173,11 +165,3 @@ export class PasskeyRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// Export an instance or the class itself depending on usage pattern
|
||||
// If used as a singleton service, export an instance:
|
||||
// export const passkeyRepository = new PasskeyRepository();
|
||||
// If instantiated elsewhere (e.g., dependency injection), export the class:
|
||||
// export { PasskeyRepository };
|
||||
// For now, let's assume it's used like other repositories (exporting functions/class)
|
||||
// Exporting the class seems more appropriate given its structure
|
||||
// Removed redundant export below as the class is already exported with 'export class'
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
// packages/backend/src/repositories/proxy.repository.ts
|
||||
import { Database, Statement } from 'sqlite3';
|
||||
// Import new async helpers and the instance getter
|
||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||
|
||||
// Remove top-level db instance
|
||||
// const db = getDb();
|
||||
|
||||
// 定义 Proxy 类型 (可以共享到 types 文件)
|
||||
export interface ProxyData {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -22,7 +15,6 @@ export interface ProxyData {
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
// Define the expected row structure from the database if it matches ProxyData
|
||||
type DbProxyRow = ProxyData;
|
||||
|
||||
/**
|
||||
@@ -59,14 +51,12 @@ export const createProxy = async (data: Omit<ProxyData, 'id' | 'created_at' | 'u
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, params);
|
||||
// Ensure lastID is valid before returning
|
||||
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
|
||||
throw new Error('创建代理后未能获取有效的 lastID');
|
||||
}
|
||||
return result.lastID;
|
||||
} catch (err: any) {
|
||||
console.error('Repository: 创建代理时出错:', err.message);
|
||||
// Handle potential UNIQUE constraint errors if needed (e.g., on name)
|
||||
throw new Error(`创建代理时出错: ${err.message}`);
|
||||
}
|
||||
};
|
||||
@@ -108,11 +98,8 @@ export const findProxyById = async (id: number): Promise<ProxyData | null> => {
|
||||
export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id' | 'created_at'>>): Promise<boolean> => {
|
||||
const fieldsToUpdate: { [key: string]: any } = { ...data };
|
||||
const params: any[] = [];
|
||||
|
||||
// Remove fields that should not be updated directly
|
||||
delete fieldsToUpdate.id;
|
||||
delete fieldsToUpdate.created_at;
|
||||
// updated_at will be set explicitly
|
||||
|
||||
fieldsToUpdate.updated_at = Math.floor(Date.now() / 1000);
|
||||
|
||||
@@ -121,10 +108,10 @@ export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id'
|
||||
|
||||
if (!setClauses) {
|
||||
console.warn(`[Repository] updateProxy called for ID ${id} with no fields to update.`);
|
||||
return false; // Nothing to update
|
||||
return false;
|
||||
}
|
||||
|
||||
params.push(id); // Add the ID for the WHERE clause
|
||||
params.push(id);
|
||||
|
||||
const sql = `UPDATE proxies SET ${setClauses} WHERE id = ?`;
|
||||
|
||||
@@ -134,7 +121,6 @@ export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id'
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error(`Repository: 更新代理 ${id} 时出错:`, err.message);
|
||||
// Handle potential UNIQUE constraint errors if needed
|
||||
throw new Error('更新代理记录失败');
|
||||
}
|
||||
};
|
||||
@@ -143,7 +129,6 @@ export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id'
|
||||
* 删除代理
|
||||
*/
|
||||
export const deleteProxy = async (id: number): Promise<boolean> => {
|
||||
// Note: connections table proxy_id foreign key has ON DELETE SET NULL.
|
||||
const sql = `DELETE FROM proxies WHERE id = ?`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// packages/backend/src/repositories/quick-commands.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';
|
||||
|
||||
// 定义快捷指令的接口
|
||||
export interface QuickCommand {
|
||||
@@ -11,7 +10,6 @@ export interface QuickCommand {
|
||||
updated_at: number; // Unix 时间戳 (秒)
|
||||
}
|
||||
|
||||
// Define the expected row structure from the database if it matches QuickCommand
|
||||
type DbQuickCommandRow = QuickCommand;
|
||||
|
||||
/**
|
||||
@@ -25,7 +23,6 @@ export const addQuickCommand = async (name: string | null, command: string): Pro
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [name, command]);
|
||||
// Ensure lastID is valid before returning
|
||||
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
|
||||
throw new Error('添加快捷指令后未能获取有效的 lastID');
|
||||
}
|
||||
@@ -34,7 +31,7 @@ export const addQuickCommand = async (name: string | null, command: string): Pro
|
||||
console.error('添加快捷指令时出错:', err.message);
|
||||
throw new Error('无法添加快捷指令');
|
||||
}
|
||||
}; // End of addQuickCommand
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新指定的快捷指令
|
||||
@@ -53,7 +50,7 @@ export const updateQuickCommand = async (id: number, name: string | null, comman
|
||||
console.error('更新快捷指令时出错:', err.message);
|
||||
throw new Error('无法更新快捷指令');
|
||||
}
|
||||
}; // End of updateQuickCommand
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 ID 删除指定的快捷指令
|
||||
@@ -70,7 +67,7 @@ export const deleteQuickCommand = async (id: number): Promise<boolean> => {
|
||||
console.error('删除快捷指令时出错:', err.message);
|
||||
throw new Error('无法删除快捷指令');
|
||||
}
|
||||
}; // End of deleteQuickCommand
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有快捷指令
|
||||
@@ -91,7 +88,7 @@ export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name
|
||||
console.error('获取快捷指令时出错:', err.message);
|
||||
throw new Error('无法获取快捷指令');
|
||||
}
|
||||
}; // End of getAllQuickCommands
|
||||
};
|
||||
|
||||
/**
|
||||
* 增加指定快捷指令的使用次数
|
||||
@@ -108,7 +105,7 @@ export const incrementUsageCount = async (id: number): Promise<boolean> => {
|
||||
console.error('增加快捷指令使用次数时出错:', err.message);
|
||||
throw new Error('无法增加快捷指令使用次数');
|
||||
}
|
||||
}; // End of incrementUsageCount
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 ID 查找快捷指令 (用于编辑前获取数据)
|
||||
@@ -120,9 +117,9 @@ export const findQuickCommandById = async (id: number): Promise<QuickCommand | u
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const row = await getDbRow<DbQuickCommandRow>(db, sql, [id]);
|
||||
return row; // Returns undefined if not found
|
||||
return row;
|
||||
} catch (err: any) {
|
||||
console.error('查找快捷指令时出错:', err.message);
|
||||
throw new Error('无法查找快捷指令');
|
||||
}
|
||||
}; // End of findQuickCommandById
|
||||
};
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
// 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
|
||||
import { SidebarConfig, LayoutNode, PaneName } from '../types/settings.types';
|
||||
import { CaptchaSettings } from '../types/settings.types';
|
||||
import * as sqlite3 from 'sqlite3';
|
||||
|
||||
// Define keys for specific settings
|
||||
const SIDEBAR_CONFIG_KEY = 'sidebarConfig';
|
||||
const CAPTCHA_CONFIG_KEY = 'captchaConfig'; // <-- Add key for CAPTCHA settings
|
||||
const CAPTCHA_CONFIG_KEY = 'captchaConfig';
|
||||
|
||||
export interface Setting {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// Define the expected row structure from the database if different from Setting
|
||||
// In this case, it seems Setting matches the SELECT columns.
|
||||
type DbSettingRow = Setting;
|
||||
|
||||
export const settingsRepository = {
|
||||
@@ -30,13 +26,12 @@ export const settingsRepository = {
|
||||
},
|
||||
|
||||
async getSetting(key: string): Promise<string | null> {
|
||||
console.log(`[Repository] Attempting to get setting with key: ${key}`);
|
||||
console.log(`[仓库] 尝试获取键为 ${key} 的设置`);
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
// Use the correct type for the expected row structure
|
||||
const row = await getDbRow<{ value: string }>(db, 'SELECT value FROM settings WHERE key = ?', [key]);
|
||||
const value = row ? row.value : null;
|
||||
console.log(`[Repository] Found value for key ${key}:`, value);
|
||||
console.log(`[仓库] 找到键 ${key} 的值:`, value);
|
||||
return value;
|
||||
} catch (err: any) {
|
||||
console.error(`[Repository] 获取设置项 ${key} 时出错:`, err.message);
|
||||
@@ -45,7 +40,7 @@ export const settingsRepository = {
|
||||
},
|
||||
|
||||
async setSetting(key: string, value: string): Promise<void> {
|
||||
const now = Math.floor(Date.now() / 1000); // Use seconds
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const sql = `INSERT INTO settings (key, value, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
@@ -53,27 +48,27 @@ export const settingsRepository = {
|
||||
updated_at = excluded.updated_at`;
|
||||
const params = [key, value, now, now];
|
||||
|
||||
console.log(`[Repository] Attempting to set setting. Key: ${key}, Value: ${value}`);
|
||||
console.log(`[Repository] Executing SQL: ${sql} with params: ${JSON.stringify(params)}`);
|
||||
console.log(`[仓库] 尝试设置设置项。键: ${key}, 值: ${value}`);
|
||||
console.log(`[仓库] 执行 SQL: ${sql},参数: ${JSON.stringify(params)}`);
|
||||
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, params);
|
||||
console.log(`[Repository] Successfully set setting for key: ${key}. Rows affected: ${result.changes}`);
|
||||
console.log(`[仓库] 成功设置键为 ${key} 的设置项。影响行数: ${result.changes}`);
|
||||
} catch (err: any) {
|
||||
console.error(`[Repository] 设置设置项 ${key} 时出错:`, err.message);
|
||||
throw new Error(`设置设置项 ${key} 失败`);
|
||||
}
|
||||
},
|
||||
|
||||
async deleteSetting(key: string): Promise<boolean> { // Return boolean indicating success
|
||||
console.log(`[Repository] Attempting to delete setting with key: ${key}`);
|
||||
async deleteSetting(key: string): Promise<boolean> {
|
||||
console.log(`[仓库] 尝试删除键为 ${key} 的设置`);
|
||||
const sql = 'DELETE FROM settings WHERE key = ?';
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [key]);
|
||||
console.log(`[Repository] Successfully deleted setting for key: ${key}. Rows affected: ${result.changes}`);
|
||||
return result.changes > 0; // Return true if a row was deleted
|
||||
console.log(`[仓库] 成功删除键为 ${key} 的设置。影响行数: ${result.changes}`);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error(`[Repository] 删除设置项 ${key} 时出错:`, err.message);
|
||||
throw new Error(`删除设置项 ${key} 失败`);
|
||||
@@ -81,28 +76,23 @@ export const settingsRepository = {
|
||||
},
|
||||
|
||||
async setMultipleSettings(settings: Record<string, string>): Promise<void> {
|
||||
console.log('[Repository] setMultipleSettings called with:', JSON.stringify(settings));
|
||||
// Use Promise.all with the async setSetting method
|
||||
// Note: 'this' inside map refers to the settingsRepository object correctly here
|
||||
console.log('[仓库] 调用 setMultipleSettings,参数:', JSON.stringify(settings));
|
||||
const promises = Object.entries(settings).map(([key, value]) =>
|
||||
this.setSetting(key, value)
|
||||
);
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
console.log('[Repository] setMultipleSettings finished successfully.');
|
||||
console.log('[仓库] setMultipleSettings 成功完成。');
|
||||
} catch (error) {
|
||||
console.error('[Repository] setMultipleSettings failed:', error);
|
||||
// Re-throw the error or handle it as needed
|
||||
console.error('[仓库] setMultipleSettings 失败:', error);
|
||||
throw new Error('批量设置失败');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// --- Specific Setting Getters/Setters ---
|
||||
|
||||
/**
|
||||
* 获取侧栏配置
|
||||
* @returns Promise<SidebarConfig> - Returns the parsed config or default
|
||||
*/
|
||||
export const getSidebarConfig = async (): Promise<SidebarConfig> => {
|
||||
const defaultValue: SidebarConfig = { left: [], right: [] };
|
||||
@@ -111,44 +101,38 @@ export const getSidebarConfig = async (): Promise<SidebarConfig> => {
|
||||
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)
|
||||
// TODO: 如果需要,添加更深入的验证(例如,检查项目是否为有效的 PaneName)
|
||||
return config as SidebarConfig;
|
||||
}
|
||||
console.warn(`[SettingsRepo] Invalid sidebarConfig format found in DB: ${jsonString}. Returning default.`);
|
||||
console.warn(`[设置仓库] 在数据库中发现无效的 sidebarConfig 格式: ${jsonString}。返回默认值。`);
|
||||
} catch (parseError) {
|
||||
console.error(`[SettingsRepo] Failed to parse sidebarConfig JSON from DB: ${jsonString}`, parseError);
|
||||
console.error(`[设置仓库] 从数据库解析 sidebarConfig JSON 失败: ${jsonString}`, parseError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[SettingsRepo] Error fetching sidebar config setting (key: ${SIDEBAR_CONFIG_KEY}):`, error);
|
||||
console.error(`[设置仓库] 获取侧边栏配置设置时出错 (键: ${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.');
|
||||
throw new Error('提供了无效的侧边栏配置对象。');
|
||||
}
|
||||
// TODO: Add deeper validation if needed (e.g., check PaneName validity)
|
||||
// TODO: 如果需要,添加更深入的验证(例如,检查 PaneName 的有效性)
|
||||
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.');
|
||||
console.error(`[设置仓库] 设置侧边栏配置时出错 (键: ${SIDEBAR_CONFIG_KEY}):`, error);
|
||||
throw new Error('保存侧边栏配置失败。');
|
||||
}
|
||||
};
|
||||
|
||||
// --- CAPTCHA Settings ---
|
||||
|
||||
/**
|
||||
* 获取 CAPTCHA 配置
|
||||
* @returns Promise<CaptchaSettings> - 返回解析后的配置或默认值
|
||||
@@ -158,18 +142,16 @@ export const getCaptchaConfig = async (): Promise<CaptchaSettings> => {
|
||||
enabled: false,
|
||||
provider: 'none',
|
||||
hcaptchaSiteKey: '',
|
||||
hcaptchaSecretKey: '', // Secret keys should ideally not have defaults stored directly here if possible
|
||||
hcaptchaSecretKey: '',
|
||||
recaptchaSiteKey: '',
|
||||
recaptchaSecretKey: '', // Secret keys should ideally not have defaults stored directly here if possible
|
||||
recaptchaSecretKey: '',
|
||||
};
|
||||
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,
|
||||
@@ -179,29 +161,25 @@ export const getCaptchaConfig = async (): Promise<CaptchaSettings> => {
|
||||
recaptchaSecretKey: config.recaptchaSecretKey ?? defaultValue.recaptchaSecretKey,
|
||||
} as CaptchaSettings;
|
||||
}
|
||||
console.warn(`[SettingsRepo] Invalid captchaConfig format found in DB: ${jsonString}. Returning default.`);
|
||||
console.warn(`[设置仓库] 在数据库中发现无效的 captchaConfig 格式: ${jsonString}。返回默认值。`);
|
||||
} catch (parseError) {
|
||||
console.error(`[SettingsRepo] Failed to parse captchaConfig JSON from DB: ${jsonString}`, parseError);
|
||||
console.error(`[设置仓库] 从数据库解析 captchaConfig JSON 失败: ${jsonString}`, parseError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[SettingsRepo] Error fetching captcha config setting (key: ${CAPTCHA_CONFIG_KEY}):`, error);
|
||||
console.error(`[设置仓库] 获取 CAPTCHA 配置设置时出错 (键: ${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.');
|
||||
throw new Error('提供了无效的 CAPTCHA 配置对象。');
|
||||
}
|
||||
// Ensure secret keys are strings, even if empty
|
||||
config.hcaptchaSecretKey = config.hcaptchaSecretKey || '';
|
||||
config.recaptchaSecretKey = config.recaptchaSecretKey || '';
|
||||
config.hcaptchaSiteKey = config.hcaptchaSiteKey || '';
|
||||
@@ -210,22 +188,16 @@ export const setCaptchaConfig = async (config: CaptchaSettings): Promise<void> =
|
||||
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.');
|
||||
console.error(`[设置仓库] 设置 CAPTCHA 配置时出错 (键: ${CAPTCHA_CONFIG_KEY}):`, error);
|
||||
throw new Error('保存 CAPTCHA 配置失败。');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- 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> => {
|
||||
// --- Define Default Structures Here ---
|
||||
// Use OmitIdRecursive helper type if needed, or define structure without IDs
|
||||
type OmitIdRecursive<T> = T extends object
|
||||
? { [K in keyof Omit<T, 'id'>]: OmitIdRecursive<T[K]> }
|
||||
: T;
|
||||
@@ -279,36 +251,34 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
|
||||
recaptchaSecretKey: '',
|
||||
};
|
||||
|
||||
// --- Define All Default Settings ---
|
||||
const defaultSettings: Record<string, string> = {
|
||||
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: JSON.stringify(defaultLayoutTreeStructure), // Use the defined structure
|
||||
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(defaultSidebarPanesStructure), // Use the defined structure
|
||||
[CAPTCHA_CONFIG_KEY]: JSON.stringify(defaultCaptchaSettings), // Add default CAPTCHA settings
|
||||
// Add other default settings here
|
||||
loginBanDuration: '300',
|
||||
focusSwitcherSequence: JSON.stringify(["quickCommandsSearch", "commandHistorySearch", "fileManagerSearch", "commandInput", "terminalSearch"]),
|
||||
navBarVisible: 'true',
|
||||
layoutTree: JSON.stringify(defaultLayoutTreeStructure),
|
||||
autoCopyOnSelect: 'false',
|
||||
showPopupFileEditor: 'false',
|
||||
shareFileEditorTabs: 'true',
|
||||
dockerStatusIntervalSeconds: '5',
|
||||
dockerDefaultExpand: 'false',
|
||||
statusMonitorIntervalSeconds: '3',
|
||||
[SIDEBAR_CONFIG_KEY]: JSON.stringify(defaultSidebarPanesStructure),
|
||||
[CAPTCHA_CONFIG_KEY]: JSON.stringify(defaultCaptchaSettings),
|
||||
};
|
||||
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...');
|
||||
console.log('[设置仓库] 确保默认设置存在...');
|
||||
try {
|
||||
for (const [key, value] of Object.entries(defaultSettings)) {
|
||||
await runDb(db, sqlInsertOrIgnore, [key, value, nowSeconds, nowSeconds]);
|
||||
}
|
||||
console.log('[SettingsRepo] Default settings check complete.');
|
||||
console.log('[设置仓库] 默认设置检查完成。');
|
||||
} catch (err: any) {
|
||||
console.error(`[SettingsRepo] Error ensuring default settings:`, err.message);
|
||||
throw new Error(`Failed to ensure default settings: ${err.message}`);
|
||||
console.error(`[设置仓库] 确保默认设置时出错:`, err.message);
|
||||
throw new Error(`确保默认设置失败: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
// packages/backend/src/repositories/tag.repository.ts
|
||||
import { Database, Statement } from 'sqlite3'; // Keep Statement if using prepare directly, otherwise remove
|
||||
// Import new async helpers and the instance getter
|
||||
import { Database, Statement } from 'sqlite3';
|
||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||
|
||||
// Remove top-level db instance
|
||||
// const db = getDb();
|
||||
|
||||
// 定义 Tag 类型 (可以共享到 types 文件)
|
||||
// Let's assume TagData is the correct interface for a row from the 'tags' table
|
||||
export interface TagData {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -24,7 +19,7 @@ export const findAllTags = async (): Promise<TagData[]> => {
|
||||
const rows = await allDb<TagData>(db, `SELECT * FROM tags ORDER BY name ASC`);
|
||||
return rows;
|
||||
} catch (err: any) {
|
||||
console.error('Repository: 查询标签列表时出错:', err.message);
|
||||
console.error('[仓库] 查询标签列表时出错:', err.message);
|
||||
throw new Error('获取标签列表失败');
|
||||
}
|
||||
};
|
||||
@@ -38,7 +33,7 @@ export const findTagById = async (id: number): Promise<TagData | null> => {
|
||||
const row = await getDbRow<TagData>(db, `SELECT * FROM tags WHERE id = ?`, [id]);
|
||||
return row || null;
|
||||
} catch (err: any) {
|
||||
console.error(`Repository: 查询标签 ${id} 时出错:`, err.message);
|
||||
console.error(`[仓库] 查询标签 ${id} 时出错:`, err.message);
|
||||
throw new Error('获取标签信息失败');
|
||||
}
|
||||
};
|
||||
@@ -48,19 +43,17 @@ export const findTagById = async (id: number): Promise<TagData | null> => {
|
||||
* 创建新标签
|
||||
*/
|
||||
export const createTag = async (name: string): Promise<number> => {
|
||||
const now = Math.floor(Date.now() / 1000); // Use seconds for consistency? Check table definition
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const sql = `INSERT INTO tags (name, created_at, updated_at) VALUES (?, ?, ?)`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [name, now, now]);
|
||||
// Ensure lastID is valid before returning
|
||||
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
|
||||
throw new Error('创建标签后未能获取有效的 lastID');
|
||||
}
|
||||
return result.lastID;
|
||||
} catch (err: any) {
|
||||
console.error('Repository: 创建标签时出错:', err.message);
|
||||
// Handle unique constraint error specifically if needed
|
||||
console.error('[仓库] 创建标签时出错:', err.message);
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
throw new Error(`标签名称 "${name}" 已存在。`);
|
||||
}
|
||||
@@ -79,8 +72,7 @@ export const updateTag = async (id: number, name: string): Promise<boolean> => {
|
||||
const result = await runDb(db, sql, [name, now, id]);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error(`Repository: 更新标签 ${id} 时出错:`, err.message);
|
||||
// Handle unique constraint error specifically if needed
|
||||
console.error(`[仓库] 更新标签 ${id} 时出错:`, err.message);
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
throw new Error(`标签名称 "${name}" 已存在。`);
|
||||
}
|
||||
@@ -92,15 +84,13 @@ export const updateTag = async (id: number, name: string): Promise<boolean> => {
|
||||
* 删除标签
|
||||
*/
|
||||
export const deleteTag = async (id: number): Promise<boolean> => {
|
||||
// Note: connection_tags junction table has ON DELETE CASCADE for tag_id,
|
||||
// so related entries there will be deleted automatically.
|
||||
const sql = `DELETE FROM tags WHERE id = ?`;
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [id]);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) {
|
||||
console.error(`Repository: 删除标签 ${id} 时出错:`, err.message);
|
||||
console.error(`[仓库] 删除标签 ${id} 时出错:`, err.message);
|
||||
throw new Error('删除标签失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
// packages/backend/src/repositories/terminal-theme.repository.ts
|
||||
import { Database } from 'sqlite3'; // Import Database type if needed for type hints
|
||||
import { getDbInstance, runDb, getDb, allDb } from '../database/connection'; // Import new async helpers, including getDb
|
||||
// Remove the incorrect import of DbTerminalThemeRow
|
||||
import { Database } from 'sqlite3';
|
||||
import { getDbInstance, runDb, getDb, allDb } from '../database/connection';
|
||||
import { TerminalTheme, CreateTerminalThemeDto, UpdateTerminalThemeDto } from '../types/terminal-theme.types';
|
||||
import { defaultXtermTheme } from '../config/default-themes';
|
||||
|
||||
// Define the interface for the raw database row structure
|
||||
// Interface matching the schema in schema.ts
|
||||
interface DbTerminalThemeRow {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -36,23 +31,18 @@ interface DbTerminalThemeRow {
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
// SQL_CREATE_TABLE and createTableIfNotExists removed as initialization is handled in database/connection.ts
|
||||
|
||||
// 辅助函数:将数据库行转换为 TerminalTheme 对象
|
||||
// Add type annotation for the input row
|
||||
const mapRowToTerminalTheme = (row: DbTerminalThemeRow): TerminalTheme => {
|
||||
// Basic check if row exists and has id property
|
||||
if (!row || typeof row.id === 'undefined') {
|
||||
console.error("mapRowToTerminalTheme received invalid row:", row);
|
||||
// Return a default or throw an error, depending on desired behavior
|
||||
// For now, let's throw an error to make the issue visible
|
||||
throw new Error("Invalid database row provided to mapRowToTerminalTheme");
|
||||
}
|
||||
try {
|
||||
return {
|
||||
_id: row.id.toString(),
|
||||
name: row.name,
|
||||
// Reconstruct themeData from individual columns
|
||||
themeData: {
|
||||
foreground: row.foreground ?? undefined,
|
||||
background: row.background ?? undefined,
|
||||
@@ -77,16 +67,12 @@ const mapRowToTerminalTheme = (row: DbTerminalThemeRow): TerminalTheme => {
|
||||
brightWhite: row.bright_white ?? undefined,
|
||||
},
|
||||
isPreset: row.theme_type === 'preset',
|
||||
// isSystemDefault needs to be handled differently, maybe based on name 'default'?
|
||||
// For now, let's assume it's not directly mapped or needed here.
|
||||
isSystemDefault: row.name === 'default', // Tentative mapping based on name
|
||||
isSystemDefault: row.name === 'default',
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
} catch (e: any) {
|
||||
// Log the entire row for debugging instead of the non-existent theme_data
|
||||
console.error(`Error mapping theme data for theme ID ${row.id}:`, e.message, "Raw row:", row);
|
||||
// Return a partially mapped object or throw error
|
||||
throw new Error(`Failed to map theme data for theme ID ${row.id}`);
|
||||
}
|
||||
};
|
||||
@@ -98,23 +84,20 @@ const mapRowToTerminalTheme = (row: DbTerminalThemeRow): TerminalTheme => {
|
||||
export const findAllThemes = async (): Promise<TerminalTheme[]> => {
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
// Specify the expected row type for allDb
|
||||
// Correct the ORDER BY clause to use theme_type and sort presets first
|
||||
const rows = await allDb<DbTerminalThemeRow>(db, 'SELECT * FROM terminal_themes ORDER BY CASE theme_type WHEN \'preset\' THEN 0 ELSE 1 END ASC, name ASC');
|
||||
// Filter out potential errors during mapping
|
||||
return rows.map(row => {
|
||||
try {
|
||||
return mapRowToTerminalTheme(row);
|
||||
} catch (mapError: any) {
|
||||
console.error(`Error mapping row ID ${row?.id}:`, mapError.message);
|
||||
return null; // Or handle differently
|
||||
return null;
|
||||
}
|
||||
}).filter((theme): theme is TerminalTheme => theme !== null);
|
||||
} catch (err: any) { // Add type annotation for err
|
||||
} catch (err: any) {
|
||||
console.error('查询所有终端主题失败:', err.message);
|
||||
// 添加详细错误日志
|
||||
console.error('详细错误:', err);
|
||||
throw new Error('查询终端主题失败'); // Re-throw or handle error appropriately
|
||||
throw new Error('查询终端主题失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,15 +109,13 @@ export const findAllThemes = async (): Promise<TerminalTheme[]> => {
|
||||
export const findThemeById = async (id: number): Promise<TerminalTheme | null> => {
|
||||
if (isNaN(id) || id <= 0) {
|
||||
console.error("findThemeById called with invalid ID:", id);
|
||||
return null; // Return null for invalid IDs
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
// Specify the expected row type for getDbRow
|
||||
// Use getDb instead of the non-existent getDbRow
|
||||
const row = await getDb<DbTerminalThemeRow>(db, 'SELECT * FROM terminal_themes WHERE id = ?', [id]);
|
||||
return row ? mapRowToTerminalTheme(row) : null;
|
||||
} catch (err: any) { // Add type annotation for err
|
||||
} catch (err: any) {
|
||||
console.error(`查询 ID 为 ${id} 的终端主题失败:`, err.message);
|
||||
throw new Error('查询终端主题失败');
|
||||
}
|
||||
@@ -146,10 +127,10 @@ export const findThemeById = async (id: number): Promise<TerminalTheme | null> =
|
||||
* @returns Promise<TerminalTheme> 新创建的主题
|
||||
*/
|
||||
export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<TerminalTheme> => {
|
||||
const nowSeconds = Math.floor(Date.now() / 1000); // Use seconds for DB consistency
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
const theme = themeDto.themeData;
|
||||
|
||||
// Define columns based on the DbTerminalThemeRow interface (excluding id, created_at, updated_at)
|
||||
|
||||
const columns = [
|
||||
'name', 'theme_type', 'foreground', 'background', 'cursor', 'cursor_accent',
|
||||
'selection_background', 'black', 'red', 'green', 'yellow', 'blue',
|
||||
@@ -157,14 +138,13 @@ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<Ter
|
||||
'bright_yellow', 'bright_blue', 'bright_magenta', 'bright_cyan', 'bright_white',
|
||||
'created_at', 'updated_at'
|
||||
];
|
||||
// Map themeDto data to corresponding columns, using null for missing optional values
|
||||
const values = [
|
||||
themeDto.name, 'user', // theme_type is 'user' for created themes
|
||||
themeDto.name, 'user',
|
||||
theme?.foreground ?? null, theme?.background ?? null, theme?.cursor ?? null, theme?.cursorAccent ?? null,
|
||||
theme?.selectionBackground ?? null, theme?.black ?? null, theme?.red ?? null, theme?.green ?? null, theme?.yellow ?? null, theme?.blue ?? null,
|
||||
theme?.magenta ?? null, theme?.cyan ?? null, theme?.white ?? null, theme?.brightBlack ?? null, theme?.brightRed ?? null, theme?.brightGreen ?? null,
|
||||
theme?.brightYellow ?? null, theme?.brightBlue ?? null, theme?.brightMagenta ?? null, theme?.brightCyan ?? null, theme?.brightWhite ?? null,
|
||||
nowSeconds, nowSeconds // Use seconds for timestamps
|
||||
nowSeconds, nowSeconds
|
||||
];
|
||||
const placeholders = columns.map(() => '?').join(', ');
|
||||
|
||||
@@ -175,8 +155,7 @@ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<Ter
|
||||
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, values); // Use the mapped values array
|
||||
// Ensure lastID is valid before trying to find the theme
|
||||
const result = await runDb(db, sql, values);
|
||||
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
|
||||
throw new Error('创建主题后未能获取有效的 lastID');
|
||||
}
|
||||
@@ -184,10 +163,9 @@ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<Ter
|
||||
if (newTheme) {
|
||||
return newTheme;
|
||||
} else {
|
||||
// This case might happen if findThemeById fails for some reason
|
||||
throw new Error(`创建主题后未能检索到 ID 为 ${result.lastID} 的主题`);
|
||||
}
|
||||
} catch (err: any) { // Add type annotation for err
|
||||
} catch (err: any) {
|
||||
console.error('创建新终端主题失败:', err.message);
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
throw new Error(`主题名称 "${themeDto.name}" 已存在。`);
|
||||
@@ -215,7 +193,7 @@ export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto):
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [themeDto.name, themeDataJson, now, id]);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) { // Add type annotation for err
|
||||
} catch (err: any) {
|
||||
console.error(`更新 ID 为 ${id} 的终端主题失败:`, err.message);
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
throw new Error(`主题名称 "${themeDto.name}" 已存在。`);
|
||||
@@ -231,13 +209,12 @@ export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto):
|
||||
* @returns Promise<boolean> 是否成功删除
|
||||
*/
|
||||
export const deleteTheme = async (id: number): Promise<boolean> => {
|
||||
// Correct the WHERE clause to use theme_type = 'user' instead of is_preset = 0
|
||||
const sql = 'DELETE FROM terminal_themes WHERE id = ? AND theme_type = \'user\'';
|
||||
try {
|
||||
const db = await getDbInstance();
|
||||
const result = await runDb(db, sql, [id]);
|
||||
return result.changes > 0;
|
||||
} catch (err: any) { // Add type annotation for err
|
||||
} catch (err: any) {
|
||||
console.error(`删除 ID 为 ${id} 的终端主题失败:`, err.message);
|
||||
throw new Error('删除终端主题失败');
|
||||
}
|
||||
@@ -252,20 +229,15 @@ export const initializePresetThemes = async (db: Database, presets: Array<Omit<T
|
||||
console.log('[DB Init] 开始检查并初始化预设主题...');
|
||||
// 在这里添加日志,显示总共要处理多少个预设主题
|
||||
console.log(`[DB Init] 发现 ${presets.length} 个预设主题定义。`);
|
||||
const nowSeconds = Math.floor(Date.now() / 1000); // Use seconds for DB consistency
|
||||
// const db = await getDbInstance(); // Use the passed db instance
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
|
||||
for (const preset of presets) {
|
||||
// 在循环开始时添加日志,显示正在处理哪个主题
|
||||
console.log(`[DB Init] 正在处理预设主题: "${preset.name}"`);
|
||||
try {
|
||||
// Check using name and theme_type
|
||||
const existing = await getDb<{ id: number }>(db, `SELECT id FROM terminal_themes WHERE name = ? AND theme_type = 'preset'`, [preset.name]);
|
||||
|
||||
if (!existing) {
|
||||
// 添加日志,表明正在插入新主题
|
||||
console.log(`[DB Init] 主题 "${preset.name}" 不存在,正在插入...`);
|
||||
// Map preset.themeData to individual columns
|
||||
const theme = preset.themeData;
|
||||
const columns = [
|
||||
'name', 'theme_type', 'foreground', 'background', 'cursor', 'cursor_accent',
|
||||
@@ -290,14 +262,10 @@ export const initializePresetThemes = async (db: Database, presets: Array<Omit<T
|
||||
await runDb(db, insertSql, values);
|
||||
console.log(`[DB Init] 预设主题 "${preset.name}" 已初始化到数据库。`);
|
||||
} else {
|
||||
// 取消注释并添加日志,表明主题已存在
|
||||
console.log(`[DB Init] 预设主题 "${preset.name}" 已存在,跳过初始化。`);
|
||||
}
|
||||
} catch (err: any) { // Add type annotation for err
|
||||
// Remove reference to non-existent preset_key
|
||||
} catch (err: any) {
|
||||
console.error(`[DB Init] 处理预设主题 "${preset.name}" 时出错:`, err.message);
|
||||
// Decide if one error should stop the whole process or just log and continue
|
||||
// throw err; // Uncomment to stop on first error
|
||||
}
|
||||
}
|
||||
console.log('[DB Init] 预设主题检查和初始化完成。');
|
||||
|
||||
Reference in New Issue
Block a user