diff --git a/packages/backend/src/appearance/appearance.controller.ts b/packages/backend/src/appearance/appearance.controller.ts index 501b3c4..eb47d93 100644 --- a/packages/backend/src/appearance/appearance.controller.ts +++ b/packages/backend/src/appearance/appearance.controller.ts @@ -34,7 +34,7 @@ const backgroundUpload = multer({ }, limits: { fileSize: 5 * 1024 * 1024 } // 限制文件大小为 5MB }); -// --- End Background Image Upload Config --- + /** diff --git a/packages/backend/src/appearance/appearance.routes.ts b/packages/backend/src/appearance/appearance.routes.ts index 4082bb5..1c79109 100644 --- a/packages/backend/src/appearance/appearance.routes.ts +++ b/packages/backend/src/appearance/appearance.routes.ts @@ -16,14 +16,14 @@ router.put('/', appearanceController.updateAppearanceSettingsController); // POST /api/v1/appearance/background/page - 上传页面背景图片 router.post( '/background/page', - appearanceController.uploadPageBackgroundMiddleware, // 使用 multer 中间件 + appearanceController.uploadPageBackgroundMiddleware, appearanceController.uploadPageBackgroundController ); // POST /api/v1/appearance/background/terminal - 上传终端背景图片 router.post( '/background/terminal', - appearanceController.uploadTerminalBackgroundMiddleware, // 使用 multer 中间件 + appearanceController.uploadTerminalBackgroundMiddleware, appearanceController.uploadTerminalBackgroundController ); diff --git a/packages/backend/src/audit/audit.controller.ts b/packages/backend/src/audit/audit.controller.ts index ce1dc62..6c4fcf8 100644 --- a/packages/backend/src/audit/audit.controller.ts +++ b/packages/backend/src/audit/audit.controller.ts @@ -39,7 +39,6 @@ export class AuditController { res.status(400).json({ message: '无效的 endDate 参数' }); return; } - // TODO: 可以添加对 actionType 是否有效的验证 // 将 searchTerm 传递给 service const result = await auditLogService.getLogs(limit, offset, actionType, startDate, endDate, searchTerm); @@ -52,7 +51,7 @@ export class AuditController { parsedDetails = JSON.parse(log.details); } catch (e) { console.warn(`[Audit Log] Failed to parse details for log ID ${log.id}:`, e); - parsedDetails = { raw: log.details, parseError: true }; // 保留原始字符串并标记错误 + parsedDetails = { raw: log.details, parseError: true }; } } return { ...log, details: parsedDetails }; diff --git a/packages/backend/src/audit/audit.routes.ts b/packages/backend/src/audit/audit.routes.ts index f020162..50317d2 100644 --- a/packages/backend/src/audit/audit.routes.ts +++ b/packages/backend/src/audit/audit.routes.ts @@ -1,14 +1,13 @@ import { Router } from 'express'; import { AuditController } from './audit.controller'; -import { isAuthenticated } from '../auth/auth.middleware'; // Use the correct auth middleware +import { isAuthenticated } from '../auth/auth.middleware'; const router = Router(); const auditController = new AuditController(); -// Apply auth middleware to protect the audit log endpoint router.use(isAuthenticated); -// Define route for getting audit logs + router.get('/', auditController.getAuditLogs); export default router; diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index 701c484..9ec5468 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -1,41 +1,35 @@ import { Request, Response } from 'express'; import bcrypt from 'bcrypt'; -// Import the instance getter and promisified helpers import { getDbInstance, runDb, getDb, allDb } from '../database/connection'; -import sqlite3, { RunResult } from 'sqlite3'; // Keep sqlite3 for type hints if needed import speakeasy from 'speakeasy'; import qrcode from 'qrcode'; -import { PasskeyService } from '../services/passkey.service'; // 导入 PasskeyService -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 +import { PasskeyService } from '../services/passkey.service'; +import { NotificationService } from '../services/notification.service'; +import { AuditLogService } from '../services/audit.service'; +import { ipBlacklistService } from '../services/ip-blacklist.service'; +import { captchaService } from '../services/captcha.service'; +import { settingsService } from '../services/settings.service'; -// Remove top-level db instance acquisition -// const db = getDb(); -const passkeyService = new PasskeyService(); // 实例化 PasskeyService -const notificationService = new NotificationService(); // 实例化 NotificationService -const auditLogService = new AuditLogService(); // 实例化 AuditLogService -// 用户数据结构占位符 (理想情况下应定义在共享的 types 文件中) +const passkeyService = new PasskeyService(); +const notificationService = new NotificationService(); +const auditLogService = new AuditLogService(); + interface User { id: number; username: string; - hashed_password: string; // 数据库中存储的哈希密码 - two_factor_secret?: string | null; // 2FA 密钥 (数据库中可能为 NULL) - // 其他可能的字段... + hashed_password: string; + two_factor_secret?: string | null; } -// 扩展 SessionData 接口以包含临时的 2FA 密钥 declare module 'express-session' { interface SessionData { userId?: number; username?: string; tempTwoFactorSecret?: string; requiresTwoFactor?: boolean; - currentChallenge?: string; // 用于存储 Passkey 操作的挑战 - rememberMe?: boolean; // 新增:临时存储“记住我”选项 + currentChallenge?: string; + rememberMe?: boolean; } } @@ -58,54 +52,39 @@ export const login = async (req: Request, res: Response): Promise => { if (captchaConfig.enabled) { const { captchaToken } = req.body; if (!captchaToken) { - console.log(`[AuthController] 登录尝试失败: CAPTCHA 已启用但未提供令牌 - ${username}`); - // 记录审计日志等(可选,看是否需要区分) res.status(400).json({ message: '需要提供 CAPTCHA 令牌。' }); - return; // 添加 return 语句以确保函数在此处终止 + return; } 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 + ipBlacklistService.recordFailedAttempt(clientIp); auditLogService.logAction('LOGIN_FAILURE', { username, reason: 'Invalid CAPTCHA token', ip: clientIp }); notificationService.sendNotification('LOGIN_FAILURE', { username, reason: 'Invalid CAPTCHA token', ip: clientIp }); res.status(401).json({ message: 'CAPTCHA 验证失败。' }); - return; // 添加 return 语句以确保函数在此处终止 + return; } console.log(`[AuthController] CAPTCHA 验证成功 - ${username}`); } catch (captchaError: any) { console.error(`[AuthController] CAPTCHA 验证过程中出错 (${username}):`, captchaError.message); - // 如果是配置错误或 API 请求失败,返回 500 res.status(500).json({ message: 'CAPTCHA 验证服务出错,请稍后重试或检查配置。' }); - return; // 添加 return 语句以确保函数在此处终止 + return; } } 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 db = await getDbInstance(); const user = await getDb(db, 'SELECT id, username, hashed_password, two_factor_secret FROM users WHERE username = ?', [username]); - /* Original callback logic replaced by await getDb - const user = await new Promise((resolve, reject) => { - // 查询用户,包含 2FA 密钥 - db.get('SELECT id, username, hashed_password, two_factor_secret FROM users WHERE username = ?', [username], (err, row: User) => { - if (err) { - console.error('查询用户时出错:', err.message); - return reject(new Error('数据库查询失败')); - } - resolve(row); - }); - }); - */ + if (!user) { console.log(`登录尝试失败: 用户未找到 - ${username}`); - const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP + const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 记录失败尝试 ipBlacklistService.recordFailedAttempt(clientIp); // 记录审计日志 (添加 IP) @@ -153,11 +132,11 @@ export const login = async (req: Request, res: Response): Promise => { // 根据 rememberMe 设置 cookie maxAge if (rememberMe) { - // 如果勾选了“记住我”,设置 cookie 有效期为 1 年 (毫秒) - req.session.cookie.maxAge = 315360000000; // 10 years = 10 * 365 * 24 * 60 * 60 * 1000 (Effectively permanent) + // 如果勾选了“记住我”,设置 cookie 有效期为 10 年 (毫秒) + req.session.cookie.maxAge = 315360000000; } else { // 如果未勾选,则不设置 maxAge,使其成为会话 cookie - req.session.cookie.maxAge = undefined; // 或者 null + req.session.cookie.maxAge = undefined; } res.status(200).json({ @@ -190,19 +169,7 @@ export const getAuthStatus = async (req: Request, res: Response): Promise // 查询用户的 2FA 状态 using promisified getDb const user = await getDb<{ two_factor_secret: string | null }>(db, 'SELECT two_factor_secret FROM users WHERE id = ?', [userId]); - /* Original callback logic replaced by await getDb - const user = await new Promise<{ two_factor_secret: string | null } | undefined>((resolve, reject) => { - db.get('SELECT two_factor_secret FROM users WHERE id = ?', [userId], (err, row: { two_factor_secret: string | null }) => { - if (err) { - console.error(`查询用户 ${userId} 2FA 状态时出错:`, err.message); - return reject(new Error('数据库查询失败')); - } - resolve(row); - }); - }); - */ - - // 如果找不到用户(理论上不应发生),也视为未认证 + // 如果找不到用户,也视为未认证 if (!user) { res.status(401).json({ isAuthenticated: false }); return; @@ -241,21 +208,11 @@ export const verifyLogin2FA = async (req: Request, res: Response): Promise } try { - const db = await getDbInstance(); // Get DB instance + const db = await getDbInstance(); // 获取用户的 2FA 密钥 using promisified getDb const user = await getDb(db, 'SELECT id, username, two_factor_secret FROM users WHERE id = ?', [userId]); - /* Original callback logic replaced by await getDb - const user = await new Promise((resolve, reject) => { - db.get('SELECT id, username, two_factor_secret FROM users WHERE id = ?', [userId], (err, row: User) => { - if (err) { - console.error(`查询用户 ${userId} 的 2FA 密钥时出错:`, err.message); - return reject(new Error('数据库查询失败')); - } - resolve(row); - }); - }); - */ + if (!user || !user.two_factor_secret) { console.error(`2FA 验证错误: 未找到用户 ${userId} 或未设置密钥。`); @@ -342,21 +299,10 @@ export const changePassword = async (req: Request, res: Response): Promise } try { - const db = await getDbInstance(); // Get DB instance - // Use promisified getDb + const db = await getDbInstance(); const user = await getDb(db, 'SELECT id, hashed_password FROM users WHERE id = ?', [userId]); - /* Original callback logic replaced by await getDb - const user = await new Promise((resolve, reject) => { - db.get('SELECT id, hashed_password FROM users WHERE id = ?', [userId], (err, row: User) => { - if (err) { - console.error(`查询用户 ${userId} 时出错:`, err.message); - return reject(new Error('数据库查询失败')); - } - resolve(row); - }); - }); - */ + if (!user) { console.error(`修改密码错误: 未找到 ID 为 ${userId} 的用户。`); @@ -375,7 +321,7 @@ export const changePassword = async (req: Request, res: Response): Promise const newHashedPassword = await bcrypt.hash(newPassword, saltRounds); const now = Math.floor(Date.now() / 1000); - // Use promisified runDb instead of prepare/run/finalize + const result = await runDb(db, 'UPDATE users SET hashed_password = ?, updated_at = ? WHERE id = ?', [newHashedPassword, now, userId] @@ -383,7 +329,7 @@ export const changePassword = async (req: Request, res: Response): Promise if (result.changes === 0) { console.error(`修改密码错误: 更新影响行数为 0 - 用户 ID ${userId}`); - throw new Error('未找到要更新的用户'); // Throw error to be caught below + throw new Error('未找到要更新的用户'); } console.log(`用户 ${userId} 密码已成功修改。`); @@ -413,20 +359,12 @@ export const setup2FA = async (req: Request, res: Response): Promise => { } try { - const db = await getDbInstance(); // Get DB instance + const db = await getDbInstance(); // 检查用户是否已启用 2FA using promisified getDb const user = await getDb<{ two_factor_secret: string | null }>(db, 'SELECT two_factor_secret FROM users WHERE id = ?', [userId]); const existingSecret = user ? user.two_factor_secret : null; - /* Original callback logic replaced by await getDb - const existingSecret = await new Promise((resolve, reject) => { - db.get('SELECT two_factor_secret FROM users WHERE id = ?', [userId], (err, row: { two_factor_secret: string | null }) => { - if (err) reject(err); - else resolve(row ? row.two_factor_secret : null); - }); - }); - */ - + if (existingSecret) { res.status(400).json({ message: '两步验证已启用。如需重置,请先禁用。' }); return; @@ -466,7 +404,7 @@ export const setup2FA = async (req: Request, res: Response): Promise => { }; -// --- 新增 Passkey 相关方法 --- +// --- Passkey 相关方法 --- /** * 生成 Passkey 注册选项 (POST /api/v1/auth/passkey/register-options) @@ -526,11 +464,10 @@ export const verifyPasskeyRegistration = async (req: Request, res: Response): Pr name ); - // Check if verification was successful and registrationInfo is present + if (verification.verified && verification.registrationInfo) { - const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP + const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 记录审计日志 (添加 IP) - // Use type assertion 'as any' to bypass persistent TS error for now const regInfo: any = verification.registrationInfo; auditLogService.logAction('PASSKEY_REGISTERED', { userId, passkeyId: regInfo.credentialID, name, ip: clientIp }); res.status(201).json({ message: 'Passkey 注册成功!', verified: true }); @@ -568,7 +505,7 @@ export const verifyAndActivate2FA = async (req: Request, res: Response): Promise } try { - const db = await getDbInstance(); // <<< Add this line to get the db instance + const db = await getDbInstance(); // 使用临时密钥验证用户提交的令牌 const verified = speakeasy.totp.verify({ secret: tempSecret, @@ -591,7 +528,7 @@ export const verifyAndActivate2FA = async (req: Request, res: Response): Promise } console.log(`用户 ${userId} 已成功激活两步验证。`); - const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP + const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 记录审计日志 (添加 IP) auditLogService.logAction('2FA_ENABLED', { userId, ip: clientIp }); @@ -629,17 +566,10 @@ export const disable2FA = async (req: Request, res: Response): Promise => } try { - const db = await getDbInstance(); // Get DB instance - // 1. 验证当前密码 using promisified getDb + const db = await getDbInstance(); + // 验证当前密码 const user = await getDb(db, 'SELECT id, hashed_password FROM users WHERE id = ?', [userId]); - /* Original callback logic replaced by await getDb - const user = await new Promise((resolve, reject) => { - db.get('SELECT id, hashed_password FROM users WHERE id = ?', [userId], (err, row: User) => { - if (err) reject(err); else resolve(row); - }); - }); - */ if (!user) { res.status(404).json({ message: '用户不存在。' }); return; } @@ -648,7 +578,7 @@ export const disable2FA = async (req: Request, res: Response): Promise => res.status(400).json({ message: '当前密码不正确。' }); return; } - // 2. 清除数据库中的 2FA 密钥 using promisified runDb + // 清除数据库中的 2FA 密钥 const now = Math.floor(Date.now() / 1000); const result = await runDb(db, 'UPDATE users SET two_factor_secret = NULL, updated_at = ? WHERE id = ?', @@ -661,7 +591,7 @@ export const disable2FA = async (req: Request, res: Response): Promise => } console.log(`用户 ${userId} 已成功禁用两步验证。`); - const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP + const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 记录审计日志 (添加 IP) auditLogService.logAction('2FA_DISABLED', { userId, ip: clientIp }); @@ -684,18 +614,6 @@ export const needsSetup = async (req: Request, res: Response): Promise => const row = await getDb<{ count: number }>(db, 'SELECT COUNT(*) as count FROM users'); const userCount = row ? row.count : 0; - /* Original callback logic replaced by await getDb - const userCount = await new Promise((resolve, reject) => { - db.get('SELECT COUNT(*) as count FROM users', (err, row: { count: number }) => { - if (err) { - console.error('检查 users 表时出错:', err.message); - return reject(new Error('数据库查询失败')); - } - resolve(row ? row.count : 0); // 如果表为空,row 可能为 undefined - }); - }); - */ - res.status(200).json({ needsSetup: userCount === 0 }); } catch (error) { @@ -711,7 +629,7 @@ export const needsSetup = async (req: Request, res: Response): Promise => export const setupAdmin = async (req: Request, res: Response): Promise => { const { username, password, confirmPassword } = req.body; - // 1. 基本输入验证 + // 基本输入验证 if (!username || !password || !confirmPassword) { res.status(400).json({ message: '用户名、密码和确认密码不能为空。' }); return; @@ -727,44 +645,30 @@ export const setupAdmin = async (req: Request, res: Response): Promise => try { - const db = await getDbInstance(); // Get DB instance - // 2. 检查数据库中是否已存在用户 (关键安全检查) using promisified getDb + const db = await getDbInstance(); + // 检查数据库中是否已存在用户 const row = await getDb<{ count: number }>(db, 'SELECT COUNT(*) as count FROM users'); const userCount = row ? row.count : 0; - /* Original callback logic replaced by await getDb - const userCount = await new Promise((resolve, reject) => { - db.get('SELECT COUNT(*) as count FROM users', (err, row: { count: number }) => { - if (err) { - console.error('检查 users 表时出错 (setupAdmin):', err.message); - return reject(new Error('数据库查询失败')); - } - resolve(row ? row.count : 0); - }); - }); - */ - if (userCount > 0) { console.warn('尝试在已有用户的情况下执行初始设置。'); res.status(403).json({ message: '设置已完成,无法重复执行。' }); return; } - // 3. 哈希密码 + // 哈希密码 const saltRounds = 10; const hashedPassword = await bcrypt.hash(password, saltRounds); const now = Math.floor(Date.now() / 1000); - // 4. 插入新用户 using promisified runDb + // 插入新用户 const result = await runDb(db, `INSERT INTO users (username, hashed_password, created_at, updated_at) VALUES (?, ?, ?, ?)`, [username, hashedPassword, now, now] ); - // Check if insertion was successful and get the ID if (typeof result.lastID !== 'number' || result.lastID <= 0) { - // This might happen due to UNIQUE constraint or other issues caught by runDb's error handling console.error('创建初始管理员后未能获取有效的 lastID。可能原因:用户名已存在或其他数据库错误。'); throw new Error('创建初始管理员失败,可能用户名已存在。'); } @@ -772,7 +676,7 @@ export const setupAdmin = async (req: Request, res: Response): Promise => console.log(`初始管理员账号 '${username}' (ID: ${newUser.id}) 已成功创建。`); - const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP + const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 记录审计日志 (添加 IP) auditLogService.logAction('ADMIN_SETUP_COMPLETE', { userId: newUser.id, username, ip: clientIp }); @@ -788,7 +692,7 @@ export const setupAdmin = async (req: Request, res: Response): Promise => * 处理用户登出请求 (POST /api/v1/auth/logout) */ export const logout = (req: Request, res: Response): void => { - const userId = req.session.userId; // 获取用户 ID 用于日志记录 + const userId = req.session.userId; const username = req.session.username; req.session.destroy((err) => { @@ -817,9 +721,8 @@ export const logout = (req: Request, res: Response): void => { export const getPublicCaptchaConfig = async (req: Request, res: Response): Promise => { try { console.log('[AuthController] Received request for public CAPTCHA config.'); - const fullConfig = await settingsService.getCaptchaConfig(); // Use settingsService + const fullConfig = await settingsService.getCaptchaConfig(); - // *** IMPORTANT: Filter out secret keys before sending to frontend *** const publicConfig = { enabled: fullConfig.enabled, provider: fullConfig.provider, diff --git a/packages/backend/src/auth/auth.middleware.ts b/packages/backend/src/auth/auth.middleware.ts index a7dfac9..e12b10b 100644 --- a/packages/backend/src/auth/auth.middleware.ts +++ b/packages/backend/src/auth/auth.middleware.ts @@ -13,5 +13,3 @@ export const isAuthenticated = (req: Request, res: Response, next: NextFunction) } }; -// 未来可以添加基于角色的授权中间件等 -// export const isAdmin = ... diff --git a/packages/backend/src/auth/auth.routes.ts b/packages/backend/src/auth/auth.routes.ts index 684357b..d6035dc 100644 --- a/packages/backend/src/auth/auth.routes.ts +++ b/packages/backend/src/auth/auth.routes.ts @@ -6,16 +6,16 @@ import { setup2FA, verifyAndActivate2FA, disable2FA, - getAuthStatus, // 导入获取状态的方法 - generatePasskeyRegistrationOptions, // 导入 Passkey 方法 - verifyPasskeyRegistration, // 导入 Passkey 方法 - needsSetup, // 导入 needsSetup 控制器 - setupAdmin, // 导入 setupAdmin 控制器 - logout, // *** 新增:导入 logout 控制器 *** - getPublicCaptchaConfig // <-- Import public CAPTCHA config controller + getAuthStatus, + generatePasskeyRegistrationOptions, + verifyPasskeyRegistration, + needsSetup, + setupAdmin, + logout, + getPublicCaptchaConfig } from './auth.controller'; import { isAuthenticated } from './auth.middleware'; -import { ipBlacklistCheckMiddleware } from './ipBlacklistCheck.middleware'; // 导入 IP 黑名单检查中间件 +import { ipBlacklistCheckMiddleware } from './ipBlacklistCheck.middleware'; const router = Router(); @@ -64,8 +64,5 @@ router.post('/passkey/verify-registration', isAuthenticated, verifyPasskeyRegist // POST /api/v1/auth/logout - 用户登出接口 (公开访问) router.post('/logout', logout); -// 未来可以添加的其他认证相关路由 -// router.get('/status', getStatus); // 获取当前登录状态 -// router.post('/setup', setupAdmin); // 已移到上面 export default router; diff --git a/packages/backend/src/auth/ipBlacklistCheck.middleware.ts b/packages/backend/src/auth/ipBlacklistCheck.middleware.ts index 9e189be..720b746 100644 --- a/packages/backend/src/auth/ipBlacklistCheck.middleware.ts +++ b/packages/backend/src/auth/ipBlacklistCheck.middleware.ts @@ -23,7 +23,6 @@ export const ipBlacklistCheckMiddleware = async (req: Request, res: Response, ne // 可以返回更通用的错误信息,避免泄露封禁状态 res.status(403).json({ message: '访问被拒绝。' }); // 或者返回更具体的错误 - // res.status(429).json({ message: '尝试次数过多,请稍后再试。' }); return; // 显式返回 void } // IP 未被封禁,继续处理请求 diff --git a/packages/backend/src/command-history/command-history.routes.ts b/packages/backend/src/command-history/command-history.routes.ts index a0beb5b..932c9db 100644 --- a/packages/backend/src/command-history/command-history.routes.ts +++ b/packages/backend/src/command-history/command-history.routes.ts @@ -1,13 +1,11 @@ import { Router } from 'express'; import * as CommandHistoryController from './command-history.controller'; -import { isAuthenticated } from '../auth/auth.middleware'; // 使用正确的认证中间件 +import { isAuthenticated } from '../auth/auth.middleware'; const router = Router(); -// 应用认证中间件到所有命令历史记录相关的路由 router.use(isAuthenticated); -// 定义路由 router.post('/', CommandHistoryController.addCommand); // POST /api/command-history router.get('/', CommandHistoryController.getAllCommands); // GET /api/command-history router.delete('/:id', CommandHistoryController.deleteCommand); // DELETE /api/command-history/:id diff --git a/packages/backend/src/config/default-themes.ts b/packages/backend/src/config/default-themes.ts index 9730c79..15a75a4 100644 --- a/packages/backend/src/config/default-themes.ts +++ b/packages/backend/src/config/default-themes.ts @@ -6,7 +6,7 @@ export const defaultXtermTheme: ITheme = { background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#d4d4d4', - selectionBackground: '#264f78', // 使用 selectionBackground + selectionBackground: '#264f78', black: '#000000', red: '#cd3131', green: '#0dbc79', @@ -32,25 +32,23 @@ export const defaultUiTheme: Record = { '--text-color': '#333333', '--text-color-secondary': '#666666', '--border-color': '#cccccc', - '--link-color': '#8E44AD', // 现代紫色 (Amethyst 变种) - '--link-hover-color': '#B180E0', // 现代紫色 - 悬停 (更亮) - '--link-active-color': '#A06CD5', // 现代紫色 - 激活 (基础) - '--link-active-bg-color': '#F3EBFB', /* 现代紫色 - 激活背景 (非常浅) */ - '--nav-item-active-bg-color': 'var(--link-active-bg-color)', /* Added */ + '--link-color': '#8E44AD', + '--link-hover-color': '#B180E0', + '--link-active-color': '#A06CD5', + '--link-active-bg-color': '#F3EBFB', + '--nav-item-active-bg-color': 'var(--link-active-bg-color)', '--header-bg-color': '#f0f0f0', '--footer-bg-color': '#f0f0f0', - '--button-bg-color': '#A06CD5', // 现代紫色 - 激活 (基础) + '--button-bg-color': '#A06CD5', '--button-text-color': '#ffffff', - '--button-hover-bg-color': '#8E44AD', // 现代紫色 - 悬停 (稍暗) - // Added new variables - '--icon-color': 'var(--text-color-secondary)', // 图标颜色 - '--icon-hover-color': 'var(--link-hover-color)', // 图标悬停颜色 (自动更新) - '--split-line-color': 'var(--border-color)', /* 分割线颜色 */ - '--split-line-hover-color': 'var(--border-color)', /* 分割线悬停颜色 */ - '--input-focus-border-color': 'var(--link-active-color)', /* 输入框聚焦边框颜色 (自动更新) */ - '--input-focus-glow': 'var(--link-active-color)', /* 输入框聚焦光晕值 (自动更新) */ - '--overlay-bg-color': 'rgba(0, 0, 0, 0.6)', /* Added Overlay Background - 恢复 rgba 以支持透明度 */ - // End added variables + '--button-hover-bg-color': '#8E44AD', + '--icon-color': 'var(--text-color-secondary)', + '--icon-hover-color': 'var(--link-hover-color)', + '--split-line-color': 'var(--border-color)', + '--split-line-hover-color': 'var(--border-color)', + '--input-focus-border-color': 'var(--link-active-color)', + '--input-focus-glow': 'var(--link-active-color)', + '--overlay-bg-color': 'rgba(0, 0, 0, 0.6)', '--font-family-sans-serif': 'sans-serif', '--base-padding': '1rem', '--base-margin': '0.5rem', diff --git a/packages/backend/src/config/preset-themes-definition.ts b/packages/backend/src/config/preset-themes-definition.ts index d77e287..c9de3bc 100644 --- a/packages/backend/src/config/preset-themes-definition.ts +++ b/packages/backend/src/config/preset-themes-definition.ts @@ -1,10 +1,7 @@ -// Generated by scripts/generate-iterm-themes.js -// Source: https://github.com/mbadolato/iTerm2-Color-Schemes -// IMPORTANT: Add the 'default' theme manually to this file if needed. -import type { ITheme } from 'xterm'; + import type { TerminalTheme } from '../types/terminal-theme.types'; -// 定义预设主题数组的类型,确保包含 preset_key + type PresetThemeDefinition = Omit & { preset_key: string }; export const presetTerminalThemes: PresetThemeDefinition[] = [ diff --git a/packages/backend/src/connections/connections.controller.ts b/packages/backend/src/connections/connections.controller.ts index 5a17dc2..a34fc0c 100644 --- a/packages/backend/src/connections/connections.controller.ts +++ b/packages/backend/src/connections/connections.controller.ts @@ -1,15 +1,8 @@ import { Request, Response } from 'express'; -// Removed duplicate import import * as ConnectionService from '../services/connection.service'; -import * as SshService from '../services/ssh.service'; // 引入 SshService -import * as ImportExportService from '../services/import-export.service'; // 引入 ImportExportService -// Removed AuditLogService import and instantiation +import * as SshService from '../services/ssh.service'; +import * as ImportExportService from '../services/import-export.service'; -// --- 移除所有不再需要的导入和变量 --- -// import { Statement } from 'sqlite3'; -// import { getDb } from '../database/connection'; // Updated import path in comment -// const db = getDb(); -// --- 清理结束 --- /** @@ -17,14 +10,11 @@ import * as ImportExportService from '../services/import-export.service'; // 引 */ export const createConnection = async (req: Request, res: Response): Promise => { try { - // Controller performs minimal validation, Service layer handles detailed business logic validation. - // 将请求体传递给服务层处理 (Service layer now handles validation and audit logging) const newConnection = await ConnectionService.createConnection(req.body); res.status(201).json({ message: '连接创建成功。', connection: newConnection }); } catch (error: any) { console.error('Controller: 创建连接时发生错误:', error); - // 根据错误类型返回不同的状态码,例如验证错误返回 400 if (error.message.includes('缺少') || error.message.includes('需要提供')) { res.status(400).json({ message: error.message }); } else { @@ -81,20 +71,16 @@ export const updateConnection = async (req: Request, res: Response): Promise // 调用 SshService 进行连接测试,现在它会返回延迟 const { latency } = await SshService.testConnection(connectionId); - // 如果 SshService.testConnection 没有抛出错误,则表示成功 - // 记录审计日志 (可选,看是否需要记录测试操作) - // auditLogService.logAction('CONNECTION_TESTED', { connectionId, success: true }); res.status(200).json({ success: true, message: '连接测试成功。', latency }); // 返回延迟 } catch (error: any) { - // 记录审计日志 (可选) - // auditLogService.logAction('CONNECTION_TESTED', { connectionId, success: false, error: error.message }); console.error(`Controller: 测试连接 ${req.params.id} 时发生错误:`, error); - // SshService 会抛出包含具体原因的 Error res.status(500).json({ success: false, message: error.message || '测试连接时发生内部服务器错误。' }); } }; @@ -221,7 +200,7 @@ export const testUnsavedConnection = async (req: Request, res: Response): Promis }; -// --- TODO: 将以下逻辑迁移到 ImportExportService --- + /** * 导出所有连接配置 (GET /api/v1/connections/export) */ @@ -234,9 +213,6 @@ export const exportConnections = async (req: Request, res: Response): Promise 0) { - // Partial success or complete failure - res.status(400).json({ // Use 400 for partial success with errors + res.status(400).json({ message: `导入完成,但存在 ${result.failureCount} 个错误。成功导入 ${result.successCount} 条。`, successCount: result.successCount, failureCount: result.failureCount, errors: result.errors }); } else { - // Complete success - // Audit logging for export/import might still be relevant here or in the service - // For now, let's assume ImportExportService handles its own logging if needed - // auditLogService.logAction('CONNECTIONS_IMPORTED', { successCount: result.successCount, failureCount: result.failureCount }); // Removed from controller + res.status(200).json({ message: `导入成功完成。共导入 ${result.successCount} 条连接。`, successCount: result.successCount, @@ -279,12 +250,10 @@ export const importConnections = async (req: Request, res: Response): Promise { // Add type for req + fileFilter: (req: Request, file, cb) => { if (file.mimetype === 'application/json') { cb(null, true); } else { - // Attach error to request instead of calling cb with error directly - // This makes it easier to handle consistently and return JSON (req as any).fileValidationError = '只允许上传 JSON 文件!'; - cb(null, false); // Reject the file + cb(null, false); } } }); @@ -35,34 +33,27 @@ const upload = multer({ // 应用认证中间件到所有 /connections 路由 router.use(isAuthenticated); // 恢复认证检查 -// --- Specific routes before parameterized routes --- // GET /api/v1/connections/export - 导出连接配置 router.get('/export', exportConnections); // POST /api/v1/connections/import - 导入连接配置 router.post('/import', (req: Request, res: Response, next: NextFunction) => { - // Use multer middleware, but handle errors specifically upload.single('connectionsFile')(req, res, (err: any) => { - // Check for file filter validation error first if ((req as any).fileValidationError) { return res.status(400).json({ message: (req as any).fileValidationError }); } - // Check for other multer errors (e.g., file size limit) if (err instanceof multer.MulterError) { return res.status(400).json({ message: `文件上传错误: ${err.message}` }); } else if (err) { - // Other unexpected errors during upload console.error("Unexpected error during file upload:", err); return res.status(500).json({ message: '文件上传处理失败' }); } - // If no errors, proceed to the controller next(); }); }, importConnections); -// --- General CRUD and other routes --- // GET /api/v1/connections - 获取连接列表 router.get('/', getConnections); diff --git a/packages/backend/src/database/connection.ts b/packages/backend/src/database/connection.ts index c8fdb80..16c80ac 100644 --- a/packages/backend/src/database/connection.ts +++ b/packages/backend/src/database/connection.ts @@ -1,182 +1,118 @@ -// packages/backend/src/database/connection.ts -import sqlite3, { OPEN_READWRITE, OPEN_CREATE } from 'sqlite3'; // Import flags + +import sqlite3, { OPEN_READWRITE, OPEN_CREATE } from 'sqlite3'; import path from 'path'; import fs from 'fs'; -import * as schema from './schema'; -// Import the table definitions registry instead of individual repositories here import { tableDefinitions } from './schema.registry'; -// presetTerminalThemes might still be needed if passed directly, but likely handled in registry now -// import { presetTerminalThemes } from '../config/preset-themes-definition'; -// Removed import for default-layout-data as logic is now in settings repository -// --- Revert to original path and filename --- -// 使用 process.cwd() 获取项目根目录,然后拼接路径,确保路径一致性 -// console.log('[Connection CWD]', process.cwd()); // 移除调试日志 -// 使用 __dirname 定位到 dist/database,然后回退两级到 packages/backend,再进入 data + const dbDir = path.join(__dirname, '..', '..', 'data'); -const dbFilename = 'nexus-terminal.db'; // Revert to original filename +const dbFilename = 'nexus-terminal.db'; const dbPath = path.join(dbDir, dbFilename); -// console.log(`[DB Path] Determined database directory: ${dbDir}`); // 移除调试日志 -// console.log(`[DB Path] Determined database file path: ${dbPath}`); // 移除调试日志 -// Add logging before checking/creating directory -// console.log(`[DB FS] Checking existence of directory: ${dbDir}`); // 移除调试日志 if (!fs.existsSync(dbDir)) { - // console.log(`[DB FS] Directory does not exist. Attempting to create: ${dbDir}`); // 移除调试日志 try { fs.mkdirSync(dbDir, { recursive: true }); - // console.log(`[DB FS] Directory successfully created: ${dbDir}`); // 移除调试日志 } catch (mkdirErr: any) { - console.error(`[DB FS] Failed to create directory ${dbDir}:`, mkdirErr.message); - // Consider throwing error here to prevent proceeding if directory creation fails - throw new Error(`Failed to create database directory: ${mkdirErr.message}`); + console.error(`[数据库文件系统] 创建目录 ${dbDir} 失败:`, mkdirErr.message); + throw new Error(`创建数据库目录失败: ${mkdirErr.message}`); } } else { - // console.log(`[DB FS] Directory already exists: ${dbDir}`); // 移除调试日志 } const verboseSqlite3 = sqlite3.verbose(); let dbInstancePromise: Promise | null = null; -// --- Promisified Database Operations --- - interface RunResult { lastID: number; changes: number; } -/** - * Promisified version of db.run(). Resolves with { lastID, changes }. - */ + export const runDb = (db: sqlite3.Database, sql: string, params: any[] = []): Promise => { return new Promise((resolve, reject) => { - db.run(sql, params, function (err: Error | null) { // Use function() to access this + db.run(sql, params, function (err: Error | null) { if (err) { - console.error(`[DB Error] SQL: ${sql.substring(0, 100)}... Params: ${JSON.stringify(params)} Error: ${err.message}`); + console.error(`[数据库错误] SQL: ${sql.substring(0, 100)}... 参数: ${JSON.stringify(params)} 错误: ${err.message}`); reject(err); } else { - // 'this' context provides lastID and changes for INSERT/UPDATE/DELETE resolve({ lastID: this.lastID, changes: this.changes }); } }); }); }; -/** - * Promisified version of db.get(). Resolves with the row found, or undefined. - */ + export const getDb = (db: sqlite3.Database, sql: string, params: any[] = []): Promise => { return new Promise((resolve, reject) => { - db.get(sql, params, (err: Error | null, row: T) => { // Add type annotation for row + db.get(sql, params, (err: Error | null, row: T) => { if (err) { - console.error(`[DB Error] SQL: ${sql.substring(0, 100)}... Params: ${JSON.stringify(params)} Error: ${err.message}`); + console.error(`[数据库错误] SQL: ${sql.substring(0, 100)}... 参数: ${JSON.stringify(params)} 错误: ${err.message}`); reject(err); } else { - resolve(row); // row will be undefined if not found + resolve(row); } }); }); }; -/** - * Promisified version of db.all(). Resolves with an array of rows found. - */ + export const allDb = (db: sqlite3.Database, sql: string, params: any[] = []): Promise => { return new Promise((resolve, reject) => { - db.all(sql, params, (err: Error | null, rows: T[]) => { // Add type annotation for rows + db.all(sql, params, (err: Error | null, rows: T[]) => { if (err) { - console.error(`[DB Error] SQL: ${sql.substring(0, 100)}... Params: ${JSON.stringify(params)} Error: ${err.message}`); + console.error(`[数据库错误] SQL: ${sql.substring(0, 100)}... 参数: ${JSON.stringify(params)} 错误: ${err.message}`); reject(err); } else { - resolve(rows); // rows will be an empty array if no matches + resolve(rows); } }); }); }; -/** - * Executes the database initialization sequence: creates all tables, inserts preset/default data. - * Now returns a Promise that resolves when all initializations are complete. - * @param db The database instance - */ const runDatabaseInitializations = async (db: sqlite3.Database): Promise => { - // console.log('[DB Init] 开始数据库初始化序列...'); // 移除调试日志 - try { - // 1. Enable foreign key constraints - await runDb(db, 'PRAGMA foreign_keys = ON;'); // Use promisified runDb - // console.log('[DB Init] 外键约束已启用。'); // 移除调试日志 - - // 2. Create tables and run initializations based on the registry + await runDb(db, 'PRAGMA foreign_keys = ON;'); for (const tableDef of tableDefinitions) { - await runDb(db, tableDef.sql); // Create table (IF NOT EXISTS) - // console.log(`[DB Init] ${tableDef.name} 表已存在或已创建。`); // 移除调试日志 + await runDb(db, tableDef.sql); if (tableDef.init) { - // Pass the db instance to the init function await tableDef.init(db); } } - - // Default layout/sidebar data is now handled within settingsRepository.ensureDefaultSettingsExist - // No separate call needed here anymore. - - // Migrations (if any) would run after initial schema setup - // import { runMigrations } from './migrations'; - // await runMigrations(db); - // console.log('[DB Init] 迁移检查完成。'); - - // console.log('[DB Init] 数据库初始化序列成功完成。'); // 移除调试日志 - } catch (error) { console.error('[DB Init] 数据库初始化序列失败:', error); - // Propagate the error to stop the application startup in index.ts throw error; } }; -/** - * Gets the database instance. Initializes the connection and runs initializations if not already done. - * Returns a Promise that resolves with the database instance once ready. - */ -// Renamed original getDb to getDbInstance to avoid confusion with the promisified getDb helper + export const getDbInstance = (): Promise => { if (!dbInstancePromise) { dbInstancePromise = new Promise((resolve, reject) => { - // Remove connectionFailed flag and double check logic - - // Add logging before attempting connection - // console.log(`[DB Connection] Attempting to connect/open database file with explicit create flag: ${dbPath}`); // 移除调试日志 - // Explicitly add OPEN_READWRITE and OPEN_CREATE flags + const db = new verboseSqlite3.Database(dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, async (err) => { // Mark callback as async - // --- Strict Error Check FIRST --- + if (err) { - console.error(`[DB Connection] Error opening database file ${dbPath}:`, err.message); - // connectionFailed = true; // Remove flag setting - dbInstancePromise = null; // Reset promise on error - reject(err); // Reject the main promise - return; // Explicitly return + console.error(`[数据库连接] 打开数据库文件 ${dbPath} 时出错:`, err.message); + dbInstancePromise = null; + reject(err); + return; } - // --- End Strict Error Check --- - // Remove Double Check Flag logic - // If no error, proceed with success logging and initialization - // console.log(`[DB Connection] Successfully connected to SQLite database: ${dbPath}`); // 移除调试日志 + try { - // Wait for initializations to complete + await runDatabaseInitializations(db); - // console.log('[DB] Database initialization complete. Ready.'); // 移除调试日志 - resolve(db); // Resolve the main promise with the db instance + resolve(db); } catch (initError) { - console.error('[DB] Initialization failed after connection, closing connection...'); - // connectionFailed = true; // Remove flag setting - dbInstancePromise = null; // Reset promise on error + console.error('[数据库] 连接后初始化失败,正在关闭连接...'); + dbInstancePromise = null; db.close((closeErr) => { - if (closeErr) console.error('[DB] Error closing connection after init failure:', closeErr.message); - reject(initError); // Reject with the initialization error + if (closeErr) console.error('[数据库] 初始化失败后关闭连接时出错:', closeErr.message); + reject(initError); }); - // process.exit(1); // Consider exiting on init failure + } }); }); @@ -184,15 +120,12 @@ export const getDbInstance = (): Promise => { return dbInstancePromise; }; -// Graceful shutdown remains the same, but it might need access to the resolved instance -// Consider a way to get the instance if needed during shutdown, e.g., a global variable set after promise resolution. -// For now, it checks the promise state indirectly. -process.on('SIGINT', async () => { // Mark as async if needed + +process.on('SIGINT', async () => { if (dbInstancePromise) { console.log('[DB] 收到 SIGINT,尝试关闭数据库连接...'); try { - // We need the actual instance, not the promise, to close - // Let's assume if the promise exists, we try to resolve it to get the instance + const db = await dbInstancePromise; db.close((err) => { if (err) { @@ -212,7 +145,4 @@ process.on('SIGINT', async () => { // Mark as async if needed } }); -// Note: We now export getDbInstance (the promise for the connection) -// and the helper functions runDb, getDb, allDb. -// Files needing the db instance will call `const db = await getDbInstance();` -// and then use `await runDb(db, ...)` etc. + diff --git a/packages/backend/src/database/migrations.ts b/packages/backend/src/database/migrations.ts index a2fd074..55eb0f8 100644 --- a/packages/backend/src/database/migrations.ts +++ b/packages/backend/src/database/migrations.ts @@ -1,6 +1,5 @@ -// packages/backend/src/migrations.ts import { Database } from 'sqlite3'; -// import { getDb } from './database'; // 可能不再需要直接从这里获取 db + /** * 运行数据库迁移。 @@ -11,14 +10,8 @@ import { Database } from 'sqlite3'; export const runMigrations = (db: Database): Promise => { return new Promise((resolve) => { console.log('[Migrations] 检查数据库迁移(当前无操作)。'); - // 在这里添加未来的迁移逻辑,例如: - // db.serialize(() => { - // db.run("ALTER TABLE users ADD COLUMN last_login INTEGER;", (err) => { ... }); - // // 更多迁移步骤... - // }); - resolve(); // 立即解决,因为没有迁移要运行 + resolve(); }); }; -// 可以保留一个默认导出或根据需要移除 -// export default runMigrations; + diff --git a/packages/backend/src/docker/docker.controller.ts b/packages/backend/src/docker/docker.controller.ts index 4d8eeac..88c0ba7 100644 --- a/packages/backend/src/docker/docker.controller.ts +++ b/packages/backend/src/docker/docker.controller.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from 'express'; -import { DockerService, DockerCommand } from '../services/docker.service'; // 导入服务和命令类型 +import { DockerService, DockerCommand } from '../services/docker.service'; // 由于没有 typedi,我们将手动实例化服务或通过其他方式获取实例 // 简单起见,这里直接 new 一个实例。在实际项目中,可能需要更复杂的实例管理。 @@ -55,8 +55,6 @@ export class DockerController { // 其他执行错误,可能是 Docker 守护进程错误等 res.status(500).json({ message: error.message || 'Failed to execute Docker command.' }); // Internal Server Error } - // 注意:这里没有调用 next(error),因为我们已经处理了响应。 - // 如果希望使用统一的错误处理中间件,则应该调用 next(error)。 } } } \ No newline at end of file diff --git a/packages/backend/src/docker/docker.routes.ts b/packages/backend/src/docker/docker.routes.ts index b65107a..efc5d5a 100644 --- a/packages/backend/src/docker/docker.routes.ts +++ b/packages/backend/src/docker/docker.routes.ts @@ -4,7 +4,7 @@ import { DockerController } from './docker.controller'; import { isAuthenticated } from '../auth/auth.middleware'; const router = Router(); -const dockerController = new DockerController(); // 同样,手动实例化 +const dockerController = new DockerController(); // 应用认证中间件,确保只有登录用户才能访问 Docker 相关接口 router.use(isAuthenticated); diff --git a/packages/backend/src/i18n.ts b/packages/backend/src/i18n.ts index 89cb2f4..9c067d9 100644 --- a/packages/backend/src/i18n.ts +++ b/packages/backend/src/i18n.ts @@ -1,7 +1,7 @@ import i18next from 'i18next'; import Backend from 'i18next-fs-backend'; import path from 'path'; -import fs from 'fs'; // 导入 fs 模块 +import fs from 'fs'; // --- 动态确定支持的语言 --- const localesDir = path.join(__dirname, 'locales'); @@ -52,9 +52,6 @@ i18next return console.error('[i18next] Error during initialization:', err); } console.log('[i18next] Initialization complete. Loaded languages:', Object.keys(i18next.store.data)); - // console.log('[i18next] Example translation (en):', t('testNotification.subject', { lng: 'en' })); // Optional test - // console.log('[i18next] Example translation (zh):', t('testNotification.subject', { lng: 'zh' })); // Optional test - // console.log('[i18next] Example translation (jp):', t('testNotification.subject', { lng: 'jp' })); // Optional test for newly added lang }); export default i18next; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a2933d0..258bdfb 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,15 +1,13 @@ import express = require('express'); import { Request, Response, NextFunction, RequestHandler } from 'express'; import http from 'http'; -import fs from 'fs'; // 导入 fs 模块 -import path from 'path'; // 导入 path 模块 -import crypto from 'crypto'; // 导入 crypto 模块 -import dotenv from 'dotenv'; // 导入 dotenv +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import dotenv from 'dotenv'; import session from 'express-session'; import sessionFileStore from 'session-file-store'; -import bcrypt from 'bcrypt'; import { getDbInstance } from './database/connection'; -// import { runMigrations } from './database/migrations'; // Migrations are handled within getDbInstance import authRouter from './auth/auth.routes'; import connectionsRouter from './connections/connections.routes'; import sftpRouter from './sftp/sftp.routes'; @@ -161,10 +159,7 @@ const startServer = () => { proxy: true, // 信任反向代理设置的 X-Forwarded-Proto 头 cookie: { httpOnly: true, - // secure: 'auto' in newer versions, relies on proxy: true here - // secure: process.env.NODE_ENV === 'production', // Keep original logic, but proxy:true adjusts behavior - secure: false, // Temporarily force secure to false for debugging proxy issues - // maxAge: 默认会话期 + secure: false, } }); app.use(sessionMiddleware); diff --git a/packages/backend/src/notifications/notification.controller.ts b/packages/backend/src/notifications/notification.controller.ts index f5abbe4..d71c9f1 100644 --- a/packages/backend/src/notifications/notification.controller.ts +++ b/packages/backend/src/notifications/notification.controller.ts @@ -1,9 +1,9 @@ import { Request, Response } from 'express'; import { NotificationService } from '../services/notification.service'; import { NotificationSetting } from '../types/notification.types'; -import { AuditLogService } from '../services/audit.service'; // 引入 AuditLogService +import { AuditLogService } from '../services/audit.service'; -const auditLogService = new AuditLogService(); // 实例化 AuditLogService +const auditLogService = new AuditLogService(); export class NotificationController { private notificationService: NotificationService; @@ -18,7 +18,6 @@ export class NotificationController { const settings = await this.notificationService.getAllSettings(); res.status(200).json(settings); } catch (error: any) { - console.error("Error fetching notification settings:", error); res.status(500).json({ message: '获取通知设置失败', error: error.message }); } }; @@ -27,7 +26,6 @@ export class NotificationController { create = async (req: Request, res: Response): Promise => { const settingData: Omit = req.body; - // Basic validation (more robust validation can be added) if (!settingData.channel_type || !settingData.name || !settingData.config) { res.status(400).json({ message: '缺少必要的通知设置字段 (channel_type, name, config)' }); return; @@ -35,14 +33,13 @@ export class NotificationController { try { const newSettingId = await this.notificationService.createSetting(settingData); - const newSetting = await this.notificationService.getSettingById(newSettingId); // Fetch the created setting to return it + const newSetting = await this.notificationService.getSettingById(newSettingId); // 记录审计日志 if (newSetting) { auditLogService.logAction('NOTIFICATION_SETTING_CREATED', { settingId: newSetting.id, name: newSetting.name, type: newSetting.channel_type }); } res.status(201).json(newSetting); } catch (error: any) { - console.error("Error creating notification setting:", error); res.status(500).json({ message: '创建通知设置失败', error: error.message }); } }; @@ -72,7 +69,6 @@ export class NotificationController { res.status(404).json({ message: `未找到 ID 为 ${id} 的通知设置` }); } } catch (error: any) { - console.error(`Error updating notification setting ID ${id}:`, error); res.status(500).json({ message: '更新通知设置失败', error: error.message }); } }; @@ -96,7 +92,6 @@ export class NotificationController { res.status(404).json({ message: `未找到 ID 为 ${id} 的通知设置` }); } } catch (error: any) { - console.error(`Error deleting notification setting ID ${id}:`, error); res.status(500).json({ message: '删除通知设置失败', error: error.message }); } }; @@ -104,7 +99,7 @@ export class NotificationController { // POST /api/v1/notifications/:id/test testSetting = async (req: Request, res: Response): Promise => { const id = parseInt(req.params.id, 10); - const { config } = req.body; // Expecting the config to test in the body + const { config } = req.body; if (isNaN(id)) { res.status(400).json({ message: '无效的通知设置 ID' }); @@ -116,28 +111,24 @@ export class NotificationController { } try { - // Fetch the original setting to determine the channel type const originalSetting = await this.notificationService.getSettingById(id); if (!originalSetting) { res.status(404).json({ message: `未找到 ID 为 ${id} 的通知设置` }); - return; // Return early if setting not found + return; } - // Call the generic testSetting method from the service, passing the channel type const result = await this.notificationService.testSetting(originalSetting.channel_type, config); if (result.success) { // 记录审计日志 (可选,根据需要决定是否记录测试操作) - // auditLogService.logAction('NOTIFICATION_SETTING_TESTED', { settingId: id, success: true }); + res.status(200).json({ message: result.message }); } else { // 记录审计日志 (可选) - // auditLogService.logAction('NOTIFICATION_SETTING_TESTED', { settingId: id, success: false, error: result.message }); - // Return 500 for test failure to indicate an issue with the config/sending + res.status(500).json({ message: result.message }); } } catch (error: any) { - console.error(`Error testing notification setting ID ${id}:`, error); res.status(500).json({ message: '测试通知设置时发生内部错误', error: error.message }); } }; @@ -158,17 +149,14 @@ export class NotificationController { } try { - // Call the generic testSetting method directly with provided type and config const result = await this.notificationService.testSetting(channel_type, config); if (result.success) { res.status(200).json({ message: result.message }); } else { - // Return 500 for test failure to indicate an issue with the config/sending res.status(500).json({ message: result.message }); } } catch (error: any) { - console.error(`Error testing unsaved notification setting:`, error); res.status(500).json({ message: '测试通知设置时发生内部错误', error: error.message }); } }; diff --git a/packages/backend/src/notifications/notification.routes.ts b/packages/backend/src/notifications/notification.routes.ts index a4b86d7..768c92c 100644 --- a/packages/backend/src/notifications/notification.routes.ts +++ b/packages/backend/src/notifications/notification.routes.ts @@ -1,23 +1,22 @@ import { Router } from 'express'; import { NotificationController } from './notification.controller'; -import { isAuthenticated } from '../auth/auth.middleware'; // Corrected import name +import { isAuthenticated } from '../auth/auth.middleware'; const router = Router(); const notificationController = new NotificationController(); -// Apply auth middleware to all notification routes + router.use(isAuthenticated); -// Define routes for notification settings CRUD + router.get('/', notificationController.getAll); router.post('/', notificationController.create); router.put('/:id', notificationController.update); router.delete('/:id', notificationController.delete); -// Route for testing a saved notification setting + router.post('/:id/test', notificationController.testSetting); -// Route for testing an unsaved notification setting configuration router.post('/test-unsaved', notificationController.testUnsavedSetting); export default router; diff --git a/packages/backend/src/proxies/proxies.controller.ts b/packages/backend/src/proxies/proxies.controller.ts index c89f31d..4988d17 100644 --- a/packages/backend/src/proxies/proxies.controller.ts +++ b/packages/backend/src/proxies/proxies.controller.ts @@ -1,10 +1,10 @@ import { Request, Response } from 'express'; import * as ProxyService from '../services/proxy.service'; -import { AuditLogService } from '../services/audit.service'; // 引入 AuditLogService +import { AuditLogService } from '../services/audit.service'; + +const auditLogService = new AuditLogService(); -const auditLogService = new AuditLogService(); // 实例化 AuditLogService -// Helper function to remove sensitive fields for response const sanitizeProxy = (proxy: ProxyService.ProxyData | null): Partial | null => { if (!proxy) return null; const { encrypted_password, encrypted_private_key, encrypted_passphrase, ...sanitized } = proxy; @@ -15,7 +15,6 @@ const sanitizeProxy = (proxy: ProxyService.ProxyData | null): Partial { try { const proxies = await ProxyService.getAllProxies(); - // Sanitize each proxy before sending res.status(200).json(proxies.map(sanitizeProxy)); } catch (error: any) { console.error('Controller: 获取代理列表失败:', error); @@ -47,7 +46,6 @@ export const getProxyById = async (req: Request, res: Response) => { // 创建新的代理配置 export const createProxy = async (req: Request, res: Response) => { try { - // Basic validation (more in service) const { name, type, host, port } = req.body; if (!name || !type || !host || !port) { return res.status(400).json({ message: '缺少必要的代理信息 (name, type, host, port)' }); @@ -61,7 +59,7 @@ export const createProxy = async (req: Request, res: Response) => { auditLogService.logAction('PROXY_CREATED', { proxyId: newProxy.id, name: newProxy.name, type: newProxy.type }); res.status(201).json({ message: '代理创建成功', - proxy: sanitizeProxy(newProxy) // Return sanitized proxy + proxy: sanitizeProxy(newProxy) }); } catch (error: any) { @@ -85,7 +83,6 @@ export const updateProxy = async (req: Request, res: Response) => { return res.status(400).json({ message: '无效的代理 ID' }); } - // Basic validation (more in service) const { name, type, host, port, username, password, auth_method, private_key, passphrase } = req.body; if (!name && !type && !host && port === undefined && username === undefined && password === undefined && auth_method === undefined && private_key === undefined && passphrase === undefined) { return res.status(400).json({ message: '没有提供任何要更新的字段' }); @@ -97,7 +94,6 @@ export const updateProxy = async (req: Request, res: Response) => { const updatedProxy = await ProxyService.updateProxy(proxyId, req.body); if (updatedProxy) { - // 记录审计日志 auditLogService.logAction('PROXY_UPDATED', { proxyId, updatedFields: Object.keys(req.body) }); res.status(200).json({ message: '代理更新成功', proxy: sanitizeProxy(updatedProxy) }); } else { diff --git a/packages/backend/src/proxies/proxies.routes.ts b/packages/backend/src/proxies/proxies.routes.ts index 2bb373c..6118749 100644 --- a/packages/backend/src/proxies/proxies.routes.ts +++ b/packages/backend/src/proxies/proxies.routes.ts @@ -1,4 +1,4 @@ -import express, { RequestHandler } from 'express'; // 引入 RequestHandler +import express, { RequestHandler } from 'express'; import { isAuthenticated } from '../auth/auth.middleware'; import { getAllProxies, @@ -6,19 +6,18 @@ import { createProxy, updateProxy, deleteProxy -} from './proxies.controller'; // 引入控制器函数 +} from './proxies.controller'; const router = express.Router(); -// 应用认证中间件到所有代理路由 + router.use(isAuthenticated); -// 定义代理 CRUD 路由 -// 显式类型断言以解决潜在的类型不匹配问题 + router.get('/', getAllProxies as RequestHandler); router.get('/:id', getProxyById as RequestHandler); router.post('/', createProxy as RequestHandler); -router.put('/:id', updateProxy as RequestHandler); // 类型断言 -router.delete('/:id', deleteProxy as RequestHandler); // 类型断言 +router.put('/:id', updateProxy as RequestHandler); +router.delete('/:id', deleteProxy as RequestHandler); export default router; diff --git a/packages/backend/src/quick-commands/quick-commands.controller.ts b/packages/backend/src/quick-commands/quick-commands.controller.ts index ae734d5..b52067b 100644 --- a/packages/backend/src/quick-commands/quick-commands.controller.ts +++ b/packages/backend/src/quick-commands/quick-commands.controller.ts @@ -120,8 +120,6 @@ export const incrementUsage = async (req: Request, res: Response): Promise // 即使没找到也可能返回成功,避免不必要的错误提示 console.warn(`尝试增加不存在的快捷指令 (ID: ${id}) 的使用次数`); res.status(200).json({ message: '使用次数已记录 (或指令不存在)' }); - // 或者严格一点返回 404: - // res.status(404).json({ message: '未找到要增加使用次数的快捷指令' }); } } catch (error: any) { console.error('增加快捷指令使用次数控制器出错:', error); diff --git a/packages/backend/src/quick-commands/quick-commands.routes.ts b/packages/backend/src/quick-commands/quick-commands.routes.ts index 31e03bf..602a2f0 100644 --- a/packages/backend/src/quick-commands/quick-commands.routes.ts +++ b/packages/backend/src/quick-commands/quick-commands.routes.ts @@ -1,10 +1,10 @@ import { Router } from 'express'; import * as QuickCommandsController from './quick-commands.controller'; -import { isAuthenticated } from '../auth/auth.middleware'; // 引入认证中间件 +import { isAuthenticated } from '../auth/auth.middleware'; const router = Router(); -// 应用认证中间件到所有快捷指令相关的路由 + router.use(isAuthenticated); // 定义路由 diff --git a/packages/backend/src/repositories/appearance.repository.ts b/packages/backend/src/repositories/appearance.repository.ts index 015cebf..dd3ec53 100644 --- a/packages/backend/src/repositories/appearance.repository.ts +++ b/packages/backend/src/repositories/appearance.repository.ts @@ -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 = {}; 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 => { 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 => { 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, 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 => { 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 对象。 + * @returns {Promise} 返回包含当前外观设置的对象。 + * @throws {Error} 如果从数据库获取设置失败。 */ export const getAppearanceSettings = async (): Promise => { try { const db = await getDbInstance(); - // Fetch all rows from the key-value table + // 从键值表中获取所有行 const rows = await allDb(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 是否成功更新 + * 更新外观设置 (公共 API)。 + * 接收一个包含要更新设置的 DTO,执行必要的验证,然后调用内部更新函数。 + * @param {UpdateAppearanceDto} settingsDto - 包含要更新设置的对象。 + * @returns {Promise} 如果至少有一个设置被成功更新或插入,则返回 true,否则返回 false。 + * @throws {Error} 如果验证失败或内部更新过程中发生错误。 */ export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto): Promise => { 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 - Success status + * 内部更新外观设置函数 (供内部调用,例如在初始化或公共 API 中)。 + * 此函数直接与数据库交互,使用 INSERT OR REPLACE 来更新或插入键值对。 + * @param {sqlite3.Database} db - 活动的数据库实例。 + * @param {UpdateAppearanceDto} settingsDto - 包含要更新设置的对象。 + * @returns {Promise} 如果至少有一个设置被成功更新或插入,则返回 true,否则返回 false。 + * @throws {Error} 如果在数据库操作期间发生错误。 */ -// Internal function to update settings in the key-value table +// 在键值表中更新设置的内部函数 const updateAppearanceSettingsInternal = async (db: sqlite3.Database, settingsDto: UpdateAppearanceDto): Promise => { 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('更新外观设置失败'); } }; diff --git a/packages/backend/src/repositories/audit.repository.ts b/packages/backend/src/repositories/audit.repository.ts index f73d15e..09ff859 100644 --- a/packages/backend/src/repositories/audit.repository.ts +++ b/packages/backend/src/repositories/audit.repository.ts @@ -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 | null): Promise { - 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 { 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(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 }; + diff --git a/packages/backend/src/repositories/command-history.repository.ts b/packages/backend/src/repositories/command-history.repository.ts index f59b048..2448a89 100644 --- a/packages/backend/src/repositories/command-history.repository.ts +++ b/packages/backend/src/repositories/command-history.repository.ts @@ -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 => { 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('无法清空命令历史记录'); diff --git a/packages/backend/src/repositories/connection.repository.ts b/packages/backend/src/repositories/connection.repository.ts index 3d3f71a..17343af 100644 --- a/packages/backend/src/repositories/connection.repository.ts +++ b/packages/backend/src/repositories/connection.repository.ts @@ -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(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(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 => { 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 => { 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 & { 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 }; diff --git a/packages/backend/src/repositories/notification.repository.ts b/packages/backend/src/repositories/notification.repository.ts index 2e9dd7b..e7902f2 100644 --- a/packages/backend/src/repositories/notification.repository.ts +++ b/packages/backend/src/repositories/notification.repository.ts @@ -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 { try { @@ -34,8 +29,8 @@ export class NotificationSettingsRepository { const rows = await allDb(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(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 { - // Note: Query remains inefficient, consider optimization later if needed. try { const db = await getDbInstance(); const rows = await allDb(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>): Promise { - // 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 }; diff --git a/packages/backend/src/repositories/passkey.repository.ts b/packages/backend/src/repositories/passkey.repository.ts index 4dc413c..459b064 100644 --- a/packages/backend/src/repositories/passkey.repository.ts +++ b/packages/backend/src/repositories/passkey.repository.ts @@ -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[]> 所有记录的部分信息的数组 */ - // Adjust return type based on selected columns async getAllPasskeys(): Promise>> { 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>(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' diff --git a/packages/backend/src/repositories/proxy.repository.ts b/packages/backend/src/repositories/proxy.repository.ts index 7e840c8..0d2db58 100644 --- a/packages/backend/src/repositories/proxy.repository.ts +++ b/packages/backend/src/repositories/proxy.repository.ts @@ -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 => { export const updateProxy = async (id: number, data: Partial>): Promise => { 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 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 => { - // Note: connections table proxy_id foreign key has ON DELETE SET NULL. const sql = `DELETE FROM proxies WHERE id = ?`; try { const db = await getDbInstance(); diff --git a/packages/backend/src/repositories/quick-commands.repository.ts b/packages/backend/src/repositories/quick-commands.repository.ts index fb1db1f..5c05ca1 100644 --- a/packages/backend/src/repositories/quick-commands.repository.ts +++ b/packages/backend/src/repositories/quick-commands.repository.ts @@ -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 => { 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 => { console.error('增加快捷指令使用次数时出错:', err.message); throw new Error('无法增加快捷指令使用次数'); } -}; // End of incrementUsageCount +}; /** * 根据 ID 查找快捷指令 (用于编辑前获取数据) @@ -120,9 +117,9 @@ export const findQuickCommandById = async (id: number): Promise(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 +}; diff --git a/packages/backend/src/repositories/settings.repository.ts b/packages/backend/src/repositories/settings.repository.ts index cd990a2..5084ba1 100644 --- a/packages/backend/src/repositories/settings.repository.ts +++ b/packages/backend/src/repositories/settings.repository.ts @@ -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 { - 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 { - 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 { // Return boolean indicating success - console.log(`[Repository] Attempting to delete setting with key: ${key}`); + async deleteSetting(key: string): Promise { + 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): Promise { - 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 - Returns the parsed config or default */ export const getSidebarConfig = async (): Promise => { const defaultValue: SidebarConfig = { left: [], right: [] }; @@ -111,44 +101,38 @@ export const getSidebarConfig = async (): Promise => { 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 => { 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 - 返回解析后的配置或默认值 @@ -158,18 +142,16 @@ export const getCaptchaConfig = async (): Promise => { 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 => { 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 => { 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 = 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 => { - // --- Define Default Structures Here --- - // Use OmitIdRecursive helper type if needed, or define structure without IDs type OmitIdRecursive = T extends object ? { [K in keyof Omit]: OmitIdRecursive } : T; @@ -279,36 +251,34 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise< recaptchaSecretKey: '', }; - // --- Define All Default Settings --- const defaultSettings: Record = { 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}`); } }; diff --git a/packages/backend/src/repositories/tag.repository.ts b/packages/backend/src/repositories/tag.repository.ts index 34c286d..0635485 100644 --- a/packages/backend/src/repositories/tag.repository.ts +++ b/packages/backend/src/repositories/tag.repository.ts @@ -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 => { const rows = await allDb(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 => { const row = await getDbRow(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 => { * 创建新标签 */ export const createTag = async (name: string): Promise => { - 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 => { 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 => { * 删除标签 */ export const deleteTag = async (id: number): Promise => { - // 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('删除标签失败'); } }; diff --git a/packages/backend/src/repositories/terminal-theme.repository.ts b/packages/backend/src/repositories/terminal-theme.repository.ts index 32f299f..5134ee3 100644 --- a/packages/backend/src/repositories/terminal-theme.repository.ts +++ b/packages/backend/src/repositories/terminal-theme.repository.ts @@ -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 => { 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(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 => { export const findThemeById = async (id: number): Promise => { 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(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 = * @returns Promise 新创建的主题 */ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise => { - 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 '?').join(', '); @@ -175,8 +155,7 @@ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise 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 是否成功删除 */ export const deleteTheme = async (id: number): Promise => { - // 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(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 => { if (!command || command.trim().length === 0) { throw new Error('命令不能为空'); } - // 可以在此添加去重逻辑,如果不想记录重复的命令 - // const existing = await CommandHistoryRepository.findCommand(command); // 如果需要更复杂的去重逻辑 - // if (existing) { ... } // 调用 upsertCommand 来处理插入或更新时间戳 return CommandHistoryRepository.upsertCommand(command.trim()); diff --git a/packages/backend/src/services/connection.service.ts b/packages/backend/src/services/connection.service.ts index 623e042..802d1db 100644 --- a/packages/backend/src/services/connection.service.ts +++ b/packages/backend/src/services/connection.service.ts @@ -1,15 +1,14 @@ import * as ConnectionRepository from '../repositories/connection.repository'; import { encrypt, decrypt } from '../utils/crypto'; -import { AuditLogService } from '../services/audit.service'; // 导入 AuditLogService +import { AuditLogService } from '../services/audit.service'; import { ConnectionBase, ConnectionWithTags, CreateConnectionInput, UpdateConnectionInput, - FullConnectionData // Import FullConnectionData if needed internally or by repo + FullConnectionData } from '../types/connection.types'; // 从集中类型文件导入 -// Re-export types if they need to be available via this service module export type { ConnectionBase, ConnectionWithTags, CreateConnectionInput, UpdateConnectionInput }; @@ -33,9 +32,8 @@ export const getConnectionById = async (id: number): Promise => { - // 1. Validate input (basic validation, more complex validation can be added) - // Removed name validation: if (!input.name || !input.host || !input.username || !input.auth_method) { - if (!input.host || !input.username || !input.auth_method) { // Validate required fields except name + // 1. 验证输入 + if (!input.host || !input.username || !input.auth_method) { throw new Error('缺少必要的连接信息 (host, username, auth_method)。'); } if (input.auth_method === 'password' && !input.password) { @@ -44,9 +42,8 @@ export const createConnection = async (input: CreateConnectionInput): Promise typeof id === 'number' && id > 0) ?? []; if (tagIds.length > 0) { await ConnectionRepository.updateConnectionTags(newConnectionId, tagIds); } - // 6. Log audit action - // Fetch the created connection to get necessary details for logging + // 6. 记录审计操作 const newConnection = await getConnectionById(newConnectionId); if (!newConnection) { - // This should ideally not happen if creation was successful + // 如果创建成功,这理论上不应该发生 console.error(`[Audit Log Error] Failed to retrieve connection ${newConnectionId} after creation.`); throw new Error('创建连接后无法检索到该连接。'); } auditLogService.logAction('CONNECTION_CREATED', { connectionId: newConnection.id, name: newConnection.name, host: newConnection.host }); - // 7. Return the newly created connection with tags + // 7. 返回新创建的带标签的连接 return newConnection; }; @@ -100,27 +96,27 @@ export const createConnection = async (input: CreateConnectionInput): Promise => { - // 1. Fetch current connection data (including encrypted fields) to compare + // 1. 获取当前连接数据(包括加密字段)以进行比较 const currentFullConnection = await ConnectionRepository.findFullConnectionById(id); if (!currentFullConnection) { - return null; // Connection not found + return null; // 未找到连接 } - // 2. Prepare data for update + // 2. 准备更新数据 const dataToUpdate: Partial = {}; let needsCredentialUpdate = false; let newAuthMethod = input.auth_method || currentFullConnection.auth_method; - // Update non-credential fields - if (input.name !== undefined) dataToUpdate.name = input.name || ''; // Use empty string '' if name is empty string or null/undefined + // 更新非凭证字段 + if (input.name !== undefined) dataToUpdate.name = input.name || ''; // 如果 name 是空字符串或 null/undefined,则使用空字符串 '' if (input.host !== undefined) dataToUpdate.host = input.host; if (input.port !== undefined) dataToUpdate.port = input.port; if (input.username !== undefined) dataToUpdate.username = input.username; - if (input.proxy_id !== undefined) dataToUpdate.proxy_id = input.proxy_id; // Allows setting to null + if (input.proxy_id !== undefined) dataToUpdate.proxy_id = input.proxy_id; // 允许设置为 null - // Handle auth method change or credential update + // 处理认证方法更改或凭证更新 if (input.auth_method && input.auth_method !== currentFullConnection.auth_method) { - // Auth method changed + // 认证方法已更改 dataToUpdate.auth_method = input.auth_method; needsCredentialUpdate = true; if (input.auth_method === 'password') { @@ -128,16 +124,16 @@ export const updateConnection = async (id: number, input: UpdateConnectionInput) dataToUpdate.encrypted_password = encrypt(input.password); dataToUpdate.encrypted_private_key = null; dataToUpdate.encrypted_passphrase = null; - } else { // key + } else { // 密钥 if (!input.private_key) throw new Error('切换到密钥认证时需要提供 private_key。'); dataToUpdate.encrypted_private_key = encrypt(input.private_key); - // Only encrypt if passphrase is a non-empty string + // 仅当密码短语为非空字符串时才加密 dataToUpdate.encrypted_passphrase = (input.passphrase && input.passphrase.trim() !== '') ? encrypt(input.passphrase) : null; dataToUpdate.encrypted_password = null; } } else { - // Auth method did not change, check if credentials for the current method were provided - // Only encrypt and update if a non-empty string is provided + // 认证方法未更改,检查是否提供了当前方法的凭证 + // 仅当提供了非空字符串时才加密和更新 if (newAuthMethod === 'password' && input.password && input.password.trim() !== '') { dataToUpdate.encrypted_password = encrypt(input.password); needsCredentialUpdate = true; @@ -145,51 +141,51 @@ export const updateConnection = async (id: number, input: UpdateConnectionInput) let passphraseChanged = false; if (input.private_key && input.private_key.trim() !== '') { dataToUpdate.encrypted_private_key = encrypt(input.private_key); - // Passphrase must be updated (or cleared) if private key is updated - // Encrypt only if non-empty, otherwise set to null + // 如果私钥更新,则必须更新(或清除)密码短语 + // 仅当非空时加密,否则设置为 null dataToUpdate.encrypted_passphrase = (input.passphrase && input.passphrase.trim() !== '') ? encrypt(input.passphrase) : null; needsCredentialUpdate = true; - passphraseChanged = true; // Mark passphrase as handled if key changed + passphraseChanged = true; // 如果密钥更改,则将密码短语标记为已处理 } - // Handle case where only passphrase is changed (and key wasn't) - // Check if input.passphrase is defined (could be empty string to clear) + // 处理仅更改密码短语(且密钥未更改)的情况 + // 检查 input.passphrase 是否已定义(可能是空字符串以清除) if (!passphraseChanged && input.passphrase !== undefined) { - // Encrypt only if non-empty, otherwise set to null + // 仅当非空时加密,否则设置为 null dataToUpdate.encrypted_passphrase = (input.passphrase && input.passphrase.trim() !== '') ? encrypt(input.passphrase) : null; - needsCredentialUpdate = true; // Consider this a credential update + needsCredentialUpdate = true; // 将此视为凭证更新 } } } - // 3. Update connection record if there are changes + // 3. 如果有更改,则更新连接记录 const hasNonTagChanges = Object.keys(dataToUpdate).length > 0; - let updatedFieldsForAudit: string[] = []; // Track fields for audit log + let updatedFieldsForAudit: string[] = []; // 跟踪审计日志的字段 if (hasNonTagChanges) { - updatedFieldsForAudit = Object.keys(dataToUpdate); // Get fields before update call + updatedFieldsForAudit = Object.keys(dataToUpdate); // 在更新调用之前获取字段 const updated = await ConnectionRepository.updateConnection(id, dataToUpdate); if (!updated) { - // Should not happen if findFullConnectionById succeeded, but good practice + // 如果 findFullConnectionById 成功,则不应发生这种情况,但这是良好的实践 throw new Error('更新连接记录失败。'); } } - // 4. Handle tags update if tag_ids were provided + // 4. 如果提供了 tag_ids,则处理标签更新 if (input.tag_ids !== undefined) { const validTagIds = input.tag_ids.filter(tagId => typeof tagId === 'number' && tagId > 0); await ConnectionRepository.updateConnectionTags(id, validTagIds); } - // Add 'tag_ids' to audit log if they were updated + // 如果 tag_ids 已更新,则将其添加到审计日志 if (input.tag_ids !== undefined) { updatedFieldsForAudit.push('tag_ids'); } - // 5. Log audit action if any changes were made + // 5. 如果进行了任何更改,则记录审计操作 if (hasNonTagChanges || input.tag_ids !== undefined) { auditLogService.logAction('CONNECTION_UPDATED', { connectionId: id, updatedFields: updatedFieldsForAudit }); } - // 6. Fetch and return the updated connection + // 6. 获取并返回更新后的连接 return getConnectionById(id); }; @@ -200,11 +196,11 @@ export const updateConnection = async (id: number, input: UpdateConnectionInput) export const deleteConnection = async (id: number): Promise => { const deleted = await ConnectionRepository.deleteConnection(id); if (deleted) { - // Log audit action after successful deletion + // 删除成功后记录审计操作 auditLogService.logAction('CONNECTION_DELETED', { connectionId: id }); } return deleted; }; -// Note: testConnection, importConnections, exportConnections logic -// will be moved to SshService and ImportExportService respectively. +// 注意:testConnection、importConnections、exportConnections 逻辑 +// 将分别移至 SshService 和 ImportExportService。 diff --git a/packages/backend/src/services/docker.service.ts b/packages/backend/src/services/docker.service.ts index d31b365..e857e8f 100644 --- a/packages/backend/src/services/docker.service.ts +++ b/packages/backend/src/services/docker.service.ts @@ -1,7 +1,5 @@ import { exec } from 'child_process'; import { promisify } from 'util'; -// import { Service } from 'typedi'; // Removed typedi import -// import { logger } from '../utils/logger'; // Removed logger import const execAsync = promisify(exec); @@ -60,11 +58,10 @@ export class DockerService { try { // 尝试执行一个简单的 docker 命令,如 docker version await execAsync('docker version', { timeout: 2000 }); // 5秒超时 - console.log('[DockerService] Docker is available.'); // Use console.log this.isDockerAvailableCache = true; return true; } catch (error: any) { - console.warn('[DockerService] Docker check failed. Docker might not be installed or running.', { error: error.message }); // Use console.warn + this.isDockerAvailableCache = false; return false; } diff --git a/packages/backend/src/services/import-export.service.ts b/packages/backend/src/services/import-export.service.ts index e3003bc..15f174d 100644 --- a/packages/backend/src/services/import-export.service.ts +++ b/packages/backend/src/services/import-export.service.ts @@ -1,13 +1,10 @@ -// packages/backend/src/services/import-export.service.ts + import * as ConnectionRepository from '../repositories/connection.repository'; import * as ProxyRepository from '../repositories/proxy.repository'; -// Import the instance getter and helpers import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; -import { Database } from 'sqlite3'; // Import Database type -// Remove top-level db instance -// --- Interface definitions remain the same --- + interface ImportedConnectionData { name: string; host: string; @@ -36,7 +33,6 @@ export interface ImportResult { failureCount: number; errors: { connectionName?: string; message: string }[]; } -// --- End Interface definitions --- /** @@ -44,9 +40,8 @@ export interface ImportResult { */ export const exportConnections = async (): Promise => { try { - const db = await getDbInstance(); // Get DB instance + const db = await getDbInstance(); - // Define a more specific type for the row structure type ExportRow = ConnectionRepository.FullConnectionData & { proxy_db_id: number | null; proxy_name: string | null; @@ -60,7 +55,7 @@ export const exportConnections = async (): Promise => proxy_encrypted_passphrase?: string | null; }; - // Fetch connections joined with proxies using await allDb + const connectionsWithProxies = await allDb(db, `SELECT c.*, @@ -75,22 +70,22 @@ export const exportConnections = async (): Promise => ORDER BY c.name ASC` ); - // Fetch all tag associations using await allDb + const tagRows = await allDb<{ connection_id: number, tag_id: number }>(db, 'SELECT connection_id, tag_id FROM connection_tags' ); - // Create a map for easy tag lookup + const tagsMap: { [connId: number]: number[] } = {}; tagRows.forEach(row => { if (!tagsMap[row.connection_id]) tagsMap[row.connection_id] = []; tagsMap[row.connection_id].push(row.tag_id); }); - // Format data for export + const formattedData: ExportedConnectionData[] = connectionsWithProxies.map(row => { const connection: ExportedConnectionData = { - name: row.name ?? 'Unnamed', // Provide default if name is null + name: row.name ?? 'Unnamed', host: row.host, port: row.port, username: row.username, @@ -104,12 +99,12 @@ export const exportConnections = async (): Promise => if (row.proxy_db_id) { connection.proxy = { - name: row.proxy_name ?? 'Unnamed Proxy', // Provide default - type: row.proxy_type ?? 'SOCKS5', // Provide default or handle error - host: row.proxy_host ?? '', // Provide default or handle error - port: row.proxy_port ?? 0, // Provide default or handle error + name: row.proxy_name ?? 'Unnamed Proxy', + type: row.proxy_type ?? 'SOCKS5', + host: row.proxy_host ?? '', + port: row.proxy_port ?? 0, username: row.proxy_username, - auth_method: row.proxy_auth_method ?? 'none', // Provide default + auth_method: row.proxy_auth_method ?? 'none', encrypted_password: row.proxy_encrypted_password, encrypted_private_key: row.proxy_encrypted_private_key, encrypted_passphrase: row.proxy_encrypted_passphrase, @@ -122,7 +117,7 @@ export const exportConnections = async (): Promise => } catch (err: any) { console.error('Service: 导出连接时出错:', err.message); - throw new Error(`导出连接失败: ${err.message}`); // Re-throw for controller + throw new Error(`导出连接失败: ${err.message}`); } }; @@ -147,26 +142,25 @@ export const importConnections = async (fileBuffer: Buffer): Promise & { tag_ids?: number[] }> = []; - const proxyCache: { [key: string]: number } = {}; // Cache for created/found proxy IDs + const proxyCache: { [key: string]: number } = {}; + - // --- Pass 1: Validate data and prepare for insertion --- for (const connData of importedData) { try { - // Basic validation + if (!connData.name || !connData.host || !connData.port || !connData.username || !connData.auth_method) { throw new Error('缺少必要的连接字段 (name, host, port, username, auth_method)。'); } - // ... (add other validation as before) ... + let proxyIdToUse: number | null = null; - // Handle proxy (find or create) - uses async repository functions if (connData.proxy) { const proxyData = connData.proxy; if (!proxyData.name || !proxyData.type || !proxyData.host || !proxyData.port) { @@ -194,11 +188,10 @@ export const importConnections = async (fileBuffer: Buffer): Promise 0) { - // Pass the transaction-aware db instance + insertedResults = await ConnectionRepository.bulkInsertConnections(db, connectionsToInsert); successCount = insertedResults.length; } - // --- Pass 3: Associate tags --- - const insertTagSql = `INSERT OR IGNORE INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`; // Use INSERT OR IGNORE + const insertTagSql = `INSERT OR IGNORE INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`; for (const result of insertedResults) { const originalTagIds = result.originalData?.tag_ids; if (Array.isArray(originalTagIds) && originalTagIds.length > 0) { const validTagIds = originalTagIds.filter((id: any) => typeof id === 'number' && id > 0); if (validTagIds.length > 0) { const tagPromises = validTagIds.map(tagId => - runDb(db, insertTagSql, [result.connectionId, tagId]).catch(tagError => { // Use await runDb + runDb(db, insertTagSql, [result.connectionId, tagId]).catch(tagError => { console.warn(`Service: 导入连接 ${result.originalData.name}: 关联标签 ID ${tagId} 失败: ${tagError.message}`); }) ); @@ -245,20 +235,19 @@ export const importConnections = async (fileBuffer: Buffer): Promise= maxAttempts && !entry.blocked_until) { // Only block and notify if not already blocked + if (newAttempts >= maxAttempts && !entry.blocked_until) { blockedUntil = now + banDuration; shouldNotify = true; console.warn(`[IP Blacklist] IP ${ip} 登录失败次数达到 ${newAttempts} 次 (阈值 ${maxAttempts}),将被封禁 ${banDuration} 秒。`); } else if (newAttempts >= maxAttempts && entry.blocked_until) { console.log(`[IP Blacklist] IP ${ip} 再次登录失败,当前已处于封禁状态。`); - // Optionally extend ban duration here if needed } await runDb(db, @@ -115,7 +110,6 @@ export class IpBlacklistService { ); if (shouldNotify && blockedUntil) { - // Trigger notification after successful DB update notificationService.sendNotification('IP_BLOCKED', { ip: ip, attempts: newAttempts, @@ -142,7 +136,6 @@ export class IpBlacklistService { ); if (shouldNotify && blockedUntil) { - // Trigger notification after successful DB insert notificationService.sendNotification('IP_BLOCKED', { ip: ip, attempts: attempts, @@ -153,7 +146,6 @@ export class IpBlacklistService { } } catch (error: any) { console.error(`[IP Blacklist] 记录 IP ${ip} 失败尝试时出错:`, error.message); - // Avoid throwing error here to prevent login process failure due to blacklist issues } } @@ -168,7 +160,6 @@ export class IpBlacklistService { console.log(`[IP Blacklist] 已重置 IP ${ip} 的失败尝试记录。`); } catch (error: any) { console.error(`[IP Blacklist] 重置 IP ${ip} 尝试次数时出错:`, error.message); - // Avoid throwing error here } } @@ -189,9 +180,7 @@ export class IpBlacklistService { return { entries, total }; } catch (error: any) { console.error('[IP Blacklist] 获取黑名单列表时出错:', error.message); - // Return empty list on error? Or re-throw? - // throw new Error('获取黑名单列表失败'); - return { entries: [], total: 0 }; // Return empty on error + return { entries: [], total: 0 }; } } @@ -213,7 +202,7 @@ export class IpBlacklistService { } } catch (error: any) { console.error(`[IP Blacklist] 从黑名单删除 IP ${ip} 时出错:`, error.message); - throw new Error(`从黑名单删除 IP ${ip} 时出错`); // Re-throw error + throw new Error(`从黑名单删除 IP ${ip} 时出错`); } } } diff --git a/packages/backend/src/services/notification.service.ts b/packages/backend/src/services/notification.service.ts index 0adb836..29e8ac7 100644 --- a/packages/backend/src/services/notification.service.ts +++ b/packages/backend/src/services/notification.service.ts @@ -8,22 +8,20 @@ import { EmailConfig, TelegramConfig, NotificationChannelConfig, - NotificationChannelType // Import the missing type + NotificationChannelType } from '../types/notification.types'; import * as nodemailer from 'nodemailer'; -import Mail from 'nodemailer/lib/mailer'; // Import Mail type for transporter -import i18next, { defaultLng, supportedLngs } from '../i18n'; // Import i18next instance and config -import { settingsService } from './settings.service'; // Import settings service -// Removed logger import +import Mail from 'nodemailer/lib/mailer'; +import i18next, { defaultLng, supportedLngs } from '../i18n'; +import { settingsService } from './settings.service'; -// Define translation keys for test notifications for clarity const testSubjectKey = 'testNotification.subject'; const testEmailBodyKey = 'testNotification.email.body'; -const testEmailBodyHtmlKey = 'testNotification.email.bodyHtml'; // Separate key for HTML version +const testEmailBodyHtmlKey = 'testNotification.email.bodyHtml'; const testWebhookDetailsKey = 'testNotification.webhook.detailsMessage'; const testTelegramDetailsKey = 'testNotification.telegram.detailsMessage'; -const testTelegramBodyTemplateKey = 'testNotification.telegram.bodyTemplate'; // Key for the template itself +const testTelegramBodyTemplateKey = 'testNotification.telegram.bodyTemplate'; export class NotificationService { private repository: NotificationSettingsRepository; @@ -41,14 +39,10 @@ export class NotificationService { } async createSetting(settingData: Omit): Promise { - // Add validation if needed return this.repository.create(settingData); } async updateSetting(id: number, settingData: Partial>): Promise { - // Add validation if needed - // Ensure password is not overwritten if not provided explicitly? Or handle in controller/route. - // For now, we assume the full config (including potentially sensitive fields) is passed for updates if needed. return this.repository.update(id, settingData); } @@ -56,9 +50,7 @@ export class NotificationService { return this.repository.delete(id); } - // --- Test Notification Methods --- - // Generic test method dispatcher async testSetting(channelType: NotificationChannelType, config: NotificationChannelConfig): Promise<{ success: boolean; message: string }> { switch (channelType) { case 'email': @@ -68,114 +60,91 @@ export class NotificationService { case 'telegram': return this._testTelegramSetting(config as TelegramConfig); default: - console.warn(`[Notification Test] Unsupported channel type for testing: ${channelType}`); + console.warn(`[通知测试] 不支持的测试渠道类型: ${channelType}`); return { success: false, message: `不支持测试此渠道类型 (${channelType})` }; } } - // Specific test method for Email private async _testEmailSetting(config: EmailConfig): Promise<{ success: boolean; message: string }> { - console.log('[Notification Test - Email] Starting test...'); // Added log + console.log('[通知测试 - 邮件] 开始测试...'); if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) { - console.error('[Notification Test - Email] Missing required config.'); // Added log + console.error('[通知测试 - 邮件] 缺少必要的配置。'); return { success: false, message: '测试邮件失败:缺少必要的 SMTP 配置信息 (收件人, 主机, 端口, 发件人)。' }; } - // --- Fetch User Language --- let userLang = defaultLng; try { const langSetting = await settingsService.getSetting('language'); if (langSetting && supportedLngs.includes(langSetting)) { userLang = langSetting; } - console.log(`[Notification Test - Email] Using language: ${userLang}`); // Added log + console.log(`[通知测试 - 邮件] 使用语言: ${userLang}`); } catch (error) { - console.error(`[Notification Test - Email] Error fetching language setting, using default (${defaultLng}):`, error); + console.error(`[通知测试 - 邮件] 获取语言设置时出错,使用默认 (${defaultLng}):`, error); } - // --- End Fetch User Language --- - // Let TypeScript infer the options type for SMTP const transporterOptions = { host: config.smtpHost, port: config.smtpPort, - secure: config.smtpSecure ?? true, // Default to true (TLS) + secure: config.smtpSecure ?? true, auth: (config.smtpUser || config.smtpPass) ? { user: config.smtpUser, - pass: config.smtpPass, // Ensure password is included if user is present + pass: config.smtpPass, } : undefined, - // Consider adding TLS options if needed, e.g., ignore self-signed certs - // tls: { - // rejectUnauthorized: false // Use with caution! - // } }; const transporter = nodemailer.createTransport(transporterOptions); - // Translate event display name first - const eventDisplayName = i18next.t(`eventDisplay.SETTINGS_UPDATED`, { lng: userLang, defaultValue: 'SETTINGS_UPDATED' }); // Hardcoding event for test email + const eventDisplayName = i18next.t(`eventDisplay.SETTINGS_UPDATED`, { lng: userLang, defaultValue: 'SETTINGS_UPDATED' }); const mailOptions: Mail.Options = { from: config.from, - to: config.to, // Use the 'to' from config for testing - // Use i18next for subject and body, using fetched user language + to: config.to, subject: i18next.t(testSubjectKey, { lng: userLang, defaultValue: 'Nexus Terminal Test Notification ({eventDisplay})', eventDisplay: eventDisplayName }), text: i18next.t(testEmailBodyKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `This is a test email from Nexus Terminal for event '{{eventDisplay}}'.\n\nIf you received this, your SMTP configuration is working.\n\nTimestamp: {{timestamp}}`, eventDisplay: eventDisplayName }), html: i18next.t(testEmailBodyHtmlKey, { lng: userLang, timestamp: new Date().toISOString(), defaultValue: `

This is a test email from Nexus Terminal for event '{{eventDisplay}}'.

If you received this, your SMTP configuration is working.

Timestamp: {{timestamp}}

`, eventDisplay: eventDisplayName }), }; try { - console.log(`[Notification Test - Email] Attempting to send test email via ${config.smtpHost}:${config.smtpPort} to ${config.to}`); // Updated log prefix + console.log(`[通知测试 - 邮件] 尝试通过 ${config.smtpHost}:${config.smtpPort} 发送测试邮件至 ${config.to}`); const info = await transporter.sendMail(mailOptions); - console.log(`[Notification Test - Email] Test email sent successfully: ${info.messageId}`); // Updated log prefix - // Verify connection if possible (optional) - // await transporter.verify(); - // console.log('[Notification Test - Email] SMTP Connection verified.'); + console.log(`[通知测试 - 邮件] 测试邮件发送成功: ${info.messageId}`); return { success: true, message: '测试邮件发送成功!请检查收件箱。' }; } catch (error: any) { - console.error(`[Notification Test - Email] Error sending test email:`, error); // Updated log prefix + console.error(`[通知测试 - 邮件] 发送测试邮件时出错:`, error); return { success: false, message: `测试邮件发送失败: ${error.message || '未知错误'}` }; } } - // Specific test method for Webhook private async _testWebhookSetting(config: WebhookConfig): Promise<{ success: boolean; message: string }> { - console.log('[Notification Test - Webhook] Starting test...'); // Added log + console.log('[通知测试 - Webhook] 开始测试...'); if (!config.url) { - console.error('[Notification Test - Webhook] Missing URL.'); // Added log + console.error('[通知测试 - Webhook] 缺少 URL。'); return { success: false, message: '测试 Webhook 失败:缺少 URL。' }; } - // --- Fetch User Language --- let userLang = defaultLng; try { const langSetting = await settingsService.getSetting('language'); if (langSetting && supportedLngs.includes(langSetting)) { userLang = langSetting; } - console.log(`[Notification Test - Webhook] Using language: ${userLang}`); // Added log + console.log(`[通知测试 - Webhook] 使用语言: ${userLang}`); } catch (error) { - console.error(`[Notification Test - Webhook] Error fetching language setting, using default (${defaultLng}):`, error); + console.error(`[通知测试 - Webhook] 获取语言设置时出错,使用默认 (${defaultLng}):`, error); } - // --- End Fetch User Language --- - // Use a valid event type for the test payload const testPayload: NotificationPayload = { - event: 'SETTINGS_UPDATED', // Use a valid event type + event: 'SETTINGS_UPDATED', timestamp: Date.now(), - // Use i18next for the details message, using fetched user language details: { message: i18next.t(testWebhookDetailsKey, { lng: userLang, defaultValue: 'This is a test notification from Nexus Terminal (Webhook).' }) } }; - // Log the translated message safely - const translatedWebhookMessage = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details is not an object with message property'; - console.log(`[Notification Test - Webhook] Test payload created. Translated details.message:`, translatedWebhookMessage); // Added log with type check + const translatedWebhookMessage = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details 不是带有 message 属性的对象'; + console.log(`[通知测试 - Webhook] 测试负载已创建。翻译后的 details.message:`, translatedWebhookMessage); - // Use the same rendering logic as actual sending - // Translate event display name const eventDisplayName = i18next.t(`eventDisplay.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event }); - // Default body for webhook test, using single braces const defaultBody = JSON.stringify(testPayload, null, 2); - const defaultBodyTemplate = `Default: JSON payload. Use {eventDisplay}, {timestamp}, {details}.`; // Updated default template text - // Pass eventDisplayName to renderTemplate + const defaultBodyTemplate = `Default: JSON payload. Use {eventDisplay}, {timestamp}, {details}.`; const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, testPayload, defaultBody, eventDisplayName); const requestConfig: AxiosRequestConfig = { @@ -186,130 +155,107 @@ export class NotificationService { ...(config.headers || {}), }, data: requestBody, - timeout: 15000, // Slightly longer timeout for testing + timeout: 15000, }; try { - console.log(`[Notification Test - Webhook] Sending test Webhook to ${config.url}`); // Updated log prefix + console.log(`[通知测试 - Webhook] 发送测试 Webhook 到 ${config.url}`); const response = await axios(requestConfig); - console.log(`[Notification Test - Webhook] Test Webhook sent successfully to ${config.url}. Status: ${response.status}`); // Updated log prefix + console.log(`[通知测试 - Webhook] 测试 Webhook 成功发送到 ${config.url}。状态: ${response.status}`); return { success: true, message: `测试 Webhook 发送成功 (状态码: ${response.status})。` }; } catch (error: any) { const errorMessage = error.response?.data?.message || error.response?.data || error.message || '未知错误'; - console.error(`[Notification Test - Webhook] Error sending test Webhook to ${config.url}:`, errorMessage); // Updated log prefix + console.error(`[通知测试 - Webhook] 发送测试 Webhook 到 ${config.url} 时出错:`, errorMessage); return { success: false, message: `测试 Webhook 发送失败: ${errorMessage}` }; } } - // Specific test method for Telegram private async _testTelegramSetting(config: TelegramConfig): Promise<{ success: boolean; message: string }> { - console.log('[Notification Test - Telegram] Starting test...'); + console.log('[通知测试 - Telegram] 开始测试...'); if (!config.botToken || !config.chatId) { - console.error('[Notification Test - Telegram] Missing botToken or chatId.'); + console.error('[通知测试 - Telegram] 缺少 botToken 或 chatId。'); return { success: false, message: '测试 Telegram 失败:缺少机器人 Token 或聊天 ID。' }; } - // --- Fetch User Language --- let userLang = defaultLng; try { const langSetting = await settingsService.getSetting('language'); if (langSetting && supportedLngs.includes(langSetting)) { userLang = langSetting; } - console.log(`[Notification Test - Telegram] Using language: ${userLang}`); // Added log + console.log(`[通知测试 - Telegram] 使用语言: ${userLang}`); } catch (error) { - console.error(`[Notification Test - Telegram] Error fetching language setting, using default (${defaultLng}):`, error); + console.error(`[通知测试 - Telegram] 获取语言设置时出错,使用默认 (${defaultLng}):`, error); } - // --- End Fetch User Language --- - // Use a valid event type for the test payload - // Declare payload first, details will be added after translation const testPayload: NotificationPayload = { event: 'SETTINGS_UPDATED', timestamp: Date.now(), - details: undefined // Initialize details as undefined + details: undefined }; - // --- Translation Start --- - // Log options before calling t() for details message - const detailsOptions = { lng: userLang, defaultValue: 'Fallback: This is a test notification from Nexus Terminal (Telegram).' }; // Use userLang - const keyWithNamespace = `notifications:${testTelegramDetailsKey}`; // Explicitly add namespace - // console.log(`[Notification Test - Telegram] Calling i18next.t for key '${keyWithNamespace}' with options:`, detailsOptions); - const translatedDetailsMessage = i18next.t(keyWithNamespace, detailsOptions); // Use key with namespace - // console.log(`[Notification Test - Telegram] Result from i18next.t for key '${keyWithNamespace}':`, translatedDetailsMessage); - // --- Translation End --- + const detailsOptions = { lng: userLang, defaultValue: 'Fallback: This is a test notification from Nexus Terminal (Telegram).' }; + const keyWithNamespace = `notifications:${testTelegramDetailsKey}`; + const translatedDetailsMessage = i18next.t(keyWithNamespace, detailsOptions); - // Assign the translated details to the existing payload object testPayload.details = { message: translatedDetailsMessage }; - // Log the translated message safely const messageFromPayload = (typeof testPayload.details === 'object' && testPayload.details?.message) ? testPayload.details.message : 'Details is not an object with message property'; - console.log(`[Notification Test - Telegram] Test payload created. Final details.message in payload:`, messageFromPayload); // Updated log description + console.log(`[Notification Test - Telegram] Test payload created. Final details.message in payload:`, messageFromPayload); - // Use the same rendering logic as actual sending - // Get the default template from i18n, fallback to a hardcoded default if key not found - // Also explicitly specify namespace and use userLang for the template key const templateKeyWithNamespace = `notifications:${testTelegramBodyTemplateKey}`; const defaultMessageTemplateFromI18n = i18next.t(templateKeyWithNamespace, { - lng: userLang, // Use userLang - defaultValue: `Fallback Template: *Nexus Terminal Test Notification*\nEvent: \`{event}\`\nTimestamp: {timestamp}\nDetails:\n\`\`\`\n{details}\n\`\`\`` // Added Fallback prefix + lng: userLang, + defaultValue: `Fallback Template: *Nexus Terminal Test Notification*\nEvent: \`{event}\`\nTimestamp: {timestamp}\nDetails:\n\`\`\`\n{details}\n\`\`\`` }); - console.log(`[Notification Test - Telegram] Default template from i18n (using lang '${userLang}', key '${templateKeyWithNamespace}'):`, defaultMessageTemplateFromI18n); // Updated log + console.log(`[通知测试 - Telegram] 来自 i18n 的默认模板 (使用语言 '${userLang}', 键 '${templateKeyWithNamespace}'):`, defaultMessageTemplateFromI18n); - // Determine which template to use (user's or default from i18n) const templateToUse = config.messageTemplate || defaultMessageTemplateFromI18n; - console.log(`[Notification Test - Telegram] Template to render:`, templateToUse); // Added log + console.log(`[通知测试 - Telegram] 要渲染的模板:`, templateToUse); - // Translate event display name const eventDisplayName = i18next.t(`eventDisplay.${testPayload.event}`, { lng: userLang, defaultValue: testPayload.event }); - // Render the template, passing eventDisplayName const messageText = this._renderTemplate(templateToUse, testPayload, '', eventDisplayName); - console.log(`[Notification Test - Telegram] Rendered message text:`, messageText); + console.log(`[通知测试 - Telegram] 渲染的消息文本:`, messageText); const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`; try { - console.log(`[Notification Test - Telegram] Sending test Telegram message to chat ID ${config.chatId}`); // Updated log prefix + console.log(`[通知测试 - Telegram] 发送测试 Telegram 消息到聊天 ID ${config.chatId}`); const response = await axios.post(telegramApiUrl, { chat_id: config.chatId, text: messageText, - parse_mode: 'Markdown' // Add parse_mode for testing consistency - }, { timeout: 15000 }); // Slightly longer timeout for testing + parse_mode: 'Markdown' + }, { timeout: 15000 }); if (response.data?.ok) { - console.log(`[Notification Test - Telegram] Test Telegram message sent successfully.`); // Updated log prefix + console.log(`[通知测试 - Telegram] 测试 Telegram 消息发送成功。`); return { success: true, message: '测试 Telegram 消息发送成功!' }; } else { - console.error(`[Notification Test - Telegram] Telegram API returned error:`, response.data?.description); // Updated log prefix + console.error(`[通知测试 - Telegram] Telegram API 返回错误:`, response.data?.description); return { success: false, message: `测试 Telegram 发送失败: ${response.data?.description || 'API 返回失败'}` }; } } catch (error: any) { const errorMessage = error.response?.data?.description || error.response?.data || error.message || '未知错误'; - console.error(`[Notification Test - Telegram] Error sending test Telegram message:`, errorMessage); // Updated log prefix + console.error(`[通知测试 - Telegram] 发送测试 Telegram 消息时出错:`, errorMessage); return { success: false, message: `测试 Telegram 发送失败: ${errorMessage}` }; } } - // --- Core Notification Sending Logic --- - async sendNotification(event: NotificationEvent, details?: Record | string): Promise { - console.log(`[Notification] Event triggered: ${event}`, details || ''); + console.log(`[通知] 事件触发: ${event}`, details || ''); - // 1. Get user's preferred language (or default) let userLang = defaultLng; try { - // Assuming settingsService is available or needs instantiation if not singleton const langSetting = await settingsService.getSetting('language'); if (langSetting && supportedLngs.includes(langSetting)) { userLang = langSetting; } } catch (error) { - console.error(`[Notification] Error fetching language setting for event ${event}:`, error); - // Proceed with default language + console.error(`[通知] 获取事件 ${event} 的语言设置时出错:`, error); } - console.log(`[Notification] Using language '${userLang}' for event ${event}`); + console.log(`[通知] 事件 ${event} 使用语言 '${userLang}'`); const payload: NotificationPayload = { event, @@ -319,224 +265,186 @@ export class NotificationService { try { const applicableSettings = await this.repository.getEnabledByEvent(event); - console.log(`[Notification] Found ${applicableSettings.length} applicable setting(s) for event ${event}`); + console.log(`[通知] 找到 ${applicableSettings.length} 个适用于事件 ${event} 的设置`); if (applicableSettings.length === 0) { - return; // No enabled settings for this event + return; // 此事件没有启用的设置 } const sendPromises = applicableSettings.map(setting => { switch (setting.channel_type) { case 'webhook': - return this._sendWebhook(setting, payload, userLang); // Pass userLang + return this._sendWebhook(setting, payload, userLang); case 'email': - return this._sendEmail(setting, payload, userLang); // Pass userLang + return this._sendEmail(setting, payload, userLang); case 'telegram': - return this._sendTelegram(setting, payload, userLang); // Pass userLang + return this._sendTelegram(setting, payload, userLang); default: - console.warn(`[Notification] Unknown channel type: ${setting.channel_type} for setting ID ${setting.id}`); - return Promise.resolve(); // Don't fail all if one is unknown + console.warn(`[通知] 未知渠道类型: ${setting.channel_type} (设置 ID: ${setting.id})`); + return Promise.resolve(); // 如果有一个未知,不要让所有都失败 } }); - // Wait for all notifications to be attempted await Promise.allSettled(sendPromises); - console.log(`[Notification] Finished attempting notifications for event ${event}`); + console.log(`[通知] 完成尝试发送事件 ${event} 的通知`); } catch (error) { - console.error(`[Notification] Error fetching or processing settings for event ${event}:`, error); - // Decide if this error itself should trigger a notification (e.g., SERVER_ERROR) - // Be careful to avoid infinite loops + console.error(`[通知] 获取或处理事件 ${event} 的设置时出错:`, error); } } - // --- Private Sending Helpers --- - // Updated to accept eventDisplayName private _renderTemplate(template: string | undefined, payload: NotificationPayload, defaultText: string, eventDisplayName?: string): string { if (!template) return defaultText; let rendered = template; - // Replace single-brace placeholders - rendered = rendered.replace(/\{event\}/g, payload.event); // Keep original event code if needed - rendered = rendered.replace(/\{eventDisplay\}/g, eventDisplayName || payload.event); // Use translated name, fallback to original code + rendered = rendered.replace(/\{event\}/g, payload.event); + rendered = rendered.replace(/\{eventDisplay\}/g, eventDisplayName || payload.event); rendered = rendered.replace(/\{timestamp\}/g, new Date(payload.timestamp).toISOString()); const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2); rendered = rendered.replace(/\{details\}/g, detailsString); return rendered; } - // Updated to accept userLang private async _sendWebhook(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise { const config = setting.config as WebhookConfig; if (!config.url) { - console.error(`[Notification] Webhook setting ID ${setting.id} is missing URL.`); + console.error(`[通知] Webhook 设置 ID ${setting.id} 缺少 URL。`); return; } - // Translate event display name const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event }); - // Translate payload details if they match a known key structure const translatedDetails = this._translatePayloadDetails(payload.details, userLang); - const translatedPayload = { ...payload, details: translatedDetails }; // Keep original payload structure for details translation + const translatedPayload = { ...payload, details: translatedDetails }; - const defaultBody = JSON.stringify(translatedPayload, null, 2); // Default body still uses the potentially translated details - // Note: Webhook body templates might need adjustments if they expect specific structures - // Use default template text if user hasn't provided one - const defaultBodyTemplate = `Default: JSON payload. Use {eventDisplay}, {timestamp}, {details}.`; // Updated placeholder - // Pass eventDisplayName to renderTemplate + const defaultBody = JSON.stringify(translatedPayload, null, 2); + const defaultBodyTemplate = `Default: JSON payload. Use {eventDisplay}, {timestamp}, {details}.`; const requestBody = this._renderTemplate(config.bodyTemplate || defaultBodyTemplate, translatedPayload, defaultBody, eventDisplayName); const requestConfig: AxiosRequestConfig = { method: config.method || 'POST', url: config.url, headers: { - 'Content-Type': 'application/json', // Default, can be overridden by config.headers + 'Content-Type': 'application/json', ...(config.headers || {}), }, data: requestBody, - timeout: 10000, // Add a timeout (e.g., 10 seconds) + timeout: 10000, }; try { - console.log(`[Notification] Sending Webhook to ${config.url} for event ${payload.event}`); + console.log(`[通知] 发送 Webhook 到 ${config.url} (事件: ${payload.event})`); const response = await axios(requestConfig); - console.log(`[Notification] Webhook sent successfully to ${config.url}. Status: ${response.status}`); + console.log(`[通知] Webhook 成功发送到 ${config.url}。状态: ${response.status}`); } catch (error: any) { const errorMessage = error.response?.data?.message || error.response?.data || error.message; - console.error(`[Notification] Error sending Webhook to ${config.url} for setting ID ${setting.id}:`, errorMessage); + console.error(`[通知] 发送 Webhook 到 ${config.url} (设置 ID: ${setting.id}) 时出错:`, errorMessage); } } - // Updated to accept userLang private async _sendEmail(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise { const config = setting.config as EmailConfig; if (!config.to || !config.smtpHost || !config.smtpPort || !config.from) { - console.error(`[Notification] Email setting ID ${setting.id} is missing required SMTP configuration (to, smtpHost, smtpPort, from).`); + console.error(`[通知] 邮件设置 ID ${setting.id} 缺少必要的 SMTP 配置 (to, smtpHost, smtpPort, from)。`); return; - } // <-- Add missing closing brace here + } - // Let TypeScript infer the options type for SMTP const transporterOptions = { host: config.smtpHost, port: config.smtpPort, - secure: config.smtpSecure ?? true, // Default to true (TLS) + secure: config.smtpSecure ?? true, auth: (config.smtpUser || config.smtpPass) ? { user: config.smtpUser, - pass: config.smtpPass, // Ensure password is included if user is present + pass: config.smtpPass, } : undefined, - // tls: { rejectUnauthorized: false } // Add if needed for self-signed certs, USE WITH CAUTION }; const transporter = nodemailer.createTransport(transporterOptions); - // Translate subject and body using i18next - // const i18nOptions = { lng: userLang, ...payload.details }; // Original line causing error const i18nOptions: Record = { lng: userLang }; if (payload.details && typeof payload.details === 'object') { - Object.assign(i18nOptions, payload.details); // Merge details if it's an object + Object.assign(i18nOptions, payload.details); } else if (payload.details !== undefined) { - i18nOptions.details = payload.details; // Pass non-object details directly if needed + i18nOptions.details = payload.details; } - // Translate event display name first const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event }); - // Try to translate the event itself for the subject, fallback to event name - const defaultSubjectKey = `event.${payload.event}`; // This key might not exist, rely on template or default below - const defaultSubjectFallback = `Nexus Terminal Notification: {eventDisplay}`; // Use eventDisplay in fallback - const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName }); // Pass eventDisplay for interpolation in fallback + const defaultSubjectKey = `event.${payload.event}`; + const defaultSubjectFallback = `Nexus Terminal Notification: {eventDisplay}`; + const subjectText = i18next.t(defaultSubjectKey, { ...i18nOptions, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName }); - // Use default subject template from i18n if user hasn't provided one - const defaultSubjectTemplateKey = 'testNotification.subject'; // Reuse test subject key structure + const defaultSubjectTemplateKey = 'testNotification.subject'; const defaultSubjectTemplate = i18next.t(defaultSubjectTemplateKey, { lng: userLang, defaultValue: defaultSubjectFallback, eventDisplay: eventDisplayName }); - // Render the subject template, passing the translated event display name const subject = this._renderTemplate(config.subjectTemplate || defaultSubjectTemplate, payload, subjectText, eventDisplayName); - // Translate the main body content based on event type if a key exists const bodyKey = `eventBody.${payload.event}`; const detailsString = typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details || {}, null, 2); - // Use eventDisplay in the default body text const defaultBodyText = `Event: ${eventDisplayName}\nTimestamp: ${new Date(payload.timestamp).toISOString()}\nDetails:\n${detailsString}`; - // Pass eventDisplay for interpolation if the translation key uses it const body = i18next.t(bodyKey, { ...i18nOptions, defaultValue: defaultBodyText, eventDisplay: eventDisplayName }); - // Note: Email body templates are not implemented. Using translated/default text. - // If templates were implemented, we'd use _renderTemplate here too. const mailOptions: Mail.Options = { from: config.from, to: config.to, subject: subject, text: body, - // html: `

${body.replace(/\n/g, '
')}

` // Simple HTML version }; try { - console.log(`[Notification] Sending Email via ${config.smtpHost}:${config.smtpPort} to ${config.to} for event ${payload.event}`); + console.log(`[通知] 通过 ${config.smtpHost}:${config.smtpPort} 发送邮件至 ${config.to} (事件: ${payload.event})`); const info = await transporter.sendMail(mailOptions); - console.log(`[Notification] Email sent successfully to ${config.to} for setting ID ${setting.id}. Message ID: ${info.messageId}`); + console.log(`[通知] 邮件成功发送至 ${config.to} (设置 ID: ${setting.id})。消息 ID: ${info.messageId}`); } catch (error: any) { - console.error(`[Notification] Error sending email for setting ID ${setting.id} via ${config.smtpHost}:`, error); + console.error(`[通知] 通过 ${config.smtpHost} 发送邮件 (设置 ID: ${setting.id}) 时出错:`, error); } } - // Updated to accept userLang private async _sendTelegram(setting: NotificationSetting, payload: NotificationPayload, userLang: string): Promise { const config = setting.config as TelegramConfig; if (!config.botToken || !config.chatId) { - console.error(`[Notification] Telegram setting ID ${setting.id} is missing botToken or chatId.`); + console.error(`[通知] Telegram 设置 ID ${setting.id} 缺少 botToken 或 chatId。`); return; } - // Translate message using i18next - // const i18nOptions = { lng: userLang, ...payload.details }; // Original line causing error const i18nOptions: Record = { lng: userLang }; if (payload.details && typeof payload.details === 'object') { - Object.assign(i18nOptions, payload.details); // Merge details if it's an object + Object.assign(i18nOptions, payload.details); } else if (payload.details !== undefined) { - i18nOptions.details = payload.details; // Pass non-object details directly if needed + i18nOptions.details = payload.details; } - // Translate event display name first const eventDisplayName = i18next.t(`eventDisplay.${payload.event}`, { lng: userLang, defaultValue: payload.event }); - const messageKey = `eventBody.${payload.event}`; // Use same key as email body for consistency + const messageKey = `eventBody.${payload.event}`; const detailsStr = payload.details ? `\nDetails: \`\`\`\n${typeof payload.details === 'string' ? payload.details : JSON.stringify(payload.details, null, 2)}\n\`\`\`` : ''; - // Use eventDisplay in the default message template fallback const defaultMessageTemplateFallback = `*Nexus Terminal Notification*\n\nEvent: \`{eventDisplay}\`\nTimestamp: {timestamp}${detailsStr}`; - // Pass eventDisplay for interpolation if the translation key uses it const translatedBody = i18next.t(messageKey, { ...i18nOptions, defaultValue: defaultMessageTemplateFallback, eventDisplay: eventDisplayName }); - // Get the default template from i18n (using the test key structure) const defaultTemplateKey = `notifications:${testTelegramBodyTemplateKey}`; const defaultMessageTemplateFromI18n = i18next.t(defaultTemplateKey, { lng: userLang, defaultValue: translatedBody, eventDisplay: eventDisplayName }); - // Allow template override, use default template from i18n if user input is empty - // Pass eventDisplayName to renderTemplate const messageText = this._renderTemplate(config.messageTemplate || defaultMessageTemplateFromI18n, payload, translatedBody, eventDisplayName); const telegramApiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`; try { - console.log(`[Notification] Sending Telegram message to chat ID ${config.chatId} for event ${payload.event}`); + console.log(`[通知] 发送 Telegram 消息到聊天 ID ${config.chatId} (事件: ${payload.event})`); const response = await axios.post(telegramApiUrl, { chat_id: config.chatId, text: messageText, - parse_mode: 'Markdown', // Keep Markdown for actual sending, user is responsible for valid syntax - }, { timeout: 10000 }); // Add timeout - console.log(`[Notification] Telegram message sent successfully. Response OK:`, response.data?.ok); + parse_mode: 'Markdown', + }, { timeout: 10000 }); + console.log(`[通知] Telegram 消息发送成功。响应 OK:`, response.data?.ok); } catch (error: any) { const errorMessage = error.response?.data?.description || error.response?.data || error.message; - console.error(`[Notification] Error sending Telegram message for setting ID ${setting.id}:`, errorMessage); + console.error(`[通知] 发送 Telegram 消息 (设置 ID: ${setting.id}) 时出错:`, errorMessage); } } - // Helper to attempt translation of known payload structures private _translatePayloadDetails(details: any, lng: string): any { if (!details || typeof details !== 'object') { - return details; // Return as is if not an object or null/undefined + return details; } - // Example: Translate connection test results if (details.testResult === 'success' && details.connectionName) { return { ...details, @@ -550,21 +458,14 @@ export class NotificationService { }; } - // Example: Translate settings update messages (can be expanded) if (details.updatedKeys && Array.isArray(details.updatedKeys)) { if (details.updatedKeys.includes('ipWhitelist')) { return { ...details, message: i18next.t('settings.ipWhitelistUpdated', { lng, defaultValue: 'IP Whitelist updated successfully.' }) }; } - // Generic settings update return { ...details, message: i18next.t('settings.updated', { lng, defaultValue: 'Settings updated successfully.' }) }; } - // Add more translation logic for other event details structures here... - - return details; // Return original details if no specific translation logic matched + return details; } } - -// Optional: Export a singleton instance if needed throughout the backend -// export const notificationService = new NotificationService(); diff --git a/packages/backend/src/services/passkey.service.ts b/packages/backend/src/services/passkey.service.ts index f5a1d46..2578406 100644 --- a/packages/backend/src/services/passkey.service.ts +++ b/packages/backend/src/services/passkey.service.ts @@ -13,13 +13,11 @@ import type { VerifyAuthenticationResponseOpts, RegistrationResponseJSON, AuthenticationResponseJSON, - // AuthenticatorDevice is not typically needed here -} from '@simplewebauthn/server'; // Import types directly from the package +} from '@simplewebauthn/server'; import { PasskeyRepository, PasskeyRecord } from '../repositories/passkey.repository'; -import { settingsService } from './settings.service'; // Import the exported object + // 定义 Relying Party (RP) 信息 - 这些应该来自配置或设置 -// TODO: 从 SettingsService 或环境变量获取这些值 const rpName = 'Nexus Terminal'; // 重要: rpID 应该是你的网站域名 (不包含协议和端口) // 对于本地开发,通常是 'localhost' @@ -29,13 +27,11 @@ const expectedOrigin = process.env.FRONTEND_URL || 'http://localhost:5173'; // export class PasskeyService { private passkeyRepository: PasskeyRepository; - // No need to instantiate settingsService if it's an object export - // private settingsService: typeof settingsService; // Use typeof for the object type + constructor() { this.passkeyRepository = new PasskeyRepository(); - // this.settingsService = settingsService; // Assign the imported object if needed - // TODO: Load rpID, rpName, expectedOrigin using settingsService.getSetting() + } /** @@ -43,33 +39,24 @@ export class PasskeyService { */ async generateRegistrationOptions(userName: string = 'nexus-user') { // WebAuthn 需要一个用户名 // 暂时不获取已存在的凭证,允许同一用户注册多个设备 - // const existingCredentials = await this.passkeyRepository.getAllPasskeys(); + const options: GenerateRegistrationOptionsOpts = { rpName, rpID, userID: Buffer.from(userName), // userID should be a Buffer/Uint8Array userName: userName, - // 不建议排除已存在的凭证,除非有特定原因 - // excludeCredentials: existingCredentials.map(cred => ({ - // id: cred.credential_id, // 需要是 Base64URL 格式,存储时确保是这个格式 - // type: 'public-key', - // transports: cred.transports ? JSON.parse(cred.transports) : undefined, - // })), + authenticatorSelection: { - // authenticatorAttachment: 'platform', // 倾向于平台认证器 (如 Windows Hello, Touch ID) userVerification: 'preferred', // 倾向于需要用户验证 (PIN, 生物识别) residentKey: 'preferred', // 倾向于创建可发现凭证 (存储在认证器上) }, // 可选:增加超时时间 timeout: 60000, // 60 秒 - // attestation: 'none', // Temporarily remove to resolve TS error, 'none' is often default }; const registrationOptions = await generateRegistrationOptions(options); - // TODO: 需要将生成的 challenge 临时存储起来 (例如在 session 或 内存缓存中),以便后续验证 - // 这里暂时返回 challenge,让 Controller 处理存储 return registrationOptions; } @@ -105,15 +92,10 @@ export class PasskeyService { if (verification.verified && verification.registrationInfo) { - // Use type assertion to bypass strict type checking for registrationInfo properties const registrationInfo = verification.registrationInfo as any; const { credentialPublicKey, credentialID, counter } = registrationInfo; - // Optional: Access other potential properties if needed - // const { credentialDeviceType, credentialBackedUp } = registrationInfo; - // 将公钥和 ID 转换为 Base64URL 字符串存储 (如果它们还不是) - // @simplewebauthn/server 返回的是 Buffer,需要转换 const credentialIdBase64Url = Buffer.from(credentialID).toString('base64url'); const publicKeyBase64Url = Buffer.from(credentialPublicKey).toString('base64url'); @@ -140,16 +122,11 @@ export class PasskeyService { * 生成 Passkey 认证选项 (挑战) */ async generateAuthenticationOptions(): Promise> { - // 可选:可以只允许已注册的凭证进行认证 - // const allowedCredentials = (await this.passkeyRepository.getAllPasskeys()).map(cred => ({ - // id: cred.credential_id, // 确保是 Base64URL 格式 - // type: 'public-key', - // transports: cred.transports ? JSON.parse(cred.transports) : undefined, - // })); + const options: GenerateAuthenticationOptionsOpts = { rpID, - // allowCredentials: allowedCredentials, // 如果只想允许已注册的凭证 + userVerification: 'preferred', // 倾向于需要用户验证 timeout: 60000, // 60 秒 }; @@ -178,45 +155,31 @@ export class PasskeyService { throw new Error(`未找到 Credential ID 为 ${credentialIdBase64Url} 的认证器`); } - // 将存储的公钥从 Base64URL 转回 Buffer - // const authenticatorPublicKeyBuffer = Buffer.from(authenticator.public_key, 'base64url'); // Moved lookup after verification - // Prepare the verification options object - authenticator is looked up internally by the library - // based on the response's credential ID, or requires allowCredentials const verificationOptions: VerifyAuthenticationResponseOpts = { response: authenticationResponse, expectedChallenge: expectedChallenge, expectedOrigin: expectedOrigin, expectedRPID: rpID, - // We need to provide a way for the library to get the authenticator details. - // Option 1: Provide `allowCredentials` (if known beforehand) - // Option 2: Let the library handle it (requires authenticator to be discoverable/resident key) - // Option 3 (Most robust): Provide the authenticator directly after fetching it. - // The library likely uses the credential ID from the response to find the authenticator, - // especially with discoverable credentials, or requires `allowCredentials`. - // Re-adding the authenticator property based on the new error message, - // ensuring the structure matches what the library likely expects. + authenticator: { credentialID: Buffer.from(authenticator.credential_id, 'base64url'), credentialPublicKey: Buffer.from(authenticator.public_key, 'base64url'), counter: authenticator.counter, transports: authenticator.transports ? JSON.parse(authenticator.transports) : undefined, }, - requireUserVerification: true, // simplewebauthn defaults this to true now - } as any; // Use type assertion to bypass strict property check for 'authenticator' + requireUserVerification: true, + } as any; let verification: VerifiedAuthenticationResponse; try { verification = await verifyAuthenticationResponse(verificationOptions); } catch (error: any) { - // If verification fails, log the error but potentially re-throw a more generic one console.error('Passkey 认证验证时发生异常:', error); const err = error as Error; - // Check if the error is due to the authenticator not being found (already handled) if (!err.message.includes(credentialIdBase64Url)) { throw new Error(`Passkey authentication verification failed: ${err.message || err}`); } - // If error is related to authenticator not found, rethrow the original specific error throw error; } diff --git a/packages/backend/src/services/proxy.service.ts b/packages/backend/src/services/proxy.service.ts index a959ae9..630d7cb 100644 --- a/packages/backend/src/services/proxy.service.ts +++ b/packages/backend/src/services/proxy.service.ts @@ -1,23 +1,20 @@ import * as ProxyRepository from '../repositories/proxy.repository'; -import { encrypt, decrypt } from '../utils/crypto'; // Assuming crypto utils are needed +import { encrypt, decrypt } from '../utils/crypto'; -// Re-export or define types (ideally from a shared types file) export interface ProxyData extends ProxyRepository.ProxyData {} -// Input type for creating a proxy export interface CreateProxyInput { name: string; type: 'SOCKS5' | 'HTTP'; host: string; port: number; username?: string | null; - auth_method?: 'none' | 'password' | 'key'; // Optional, defaults to 'none' - password?: string | null; // Plain text password - private_key?: string | null; // Plain text private key - passphrase?: string | null; // Plain text passphrase + auth_method?: 'none' | 'password' | 'key'; + password?: string | null; + private_key?: string | null; + passphrase?: string | null; } -// Input type for updating a proxy export interface UpdateProxyInput { name?: string; type?: 'SOCKS5' | 'HTTP'; @@ -25,9 +22,9 @@ export interface UpdateProxyInput { port?: number; username?: string | null; auth_method?: 'none' | 'password' | 'key'; - password?: string | null; // Use undefined for no change, null/empty to clear - private_key?: string | null; // Use undefined for no change, null/empty to clear - passphrase?: string | null; // Use undefined for no change, null/empty to clear + password?: string | null; + private_key?: string | null; + passphrase?: string | null; } @@ -35,8 +32,6 @@ export interface UpdateProxyInput { * 获取所有代理 */ export const getAllProxies = async (): Promise => { - // Repository returns data with encrypted fields, which is fine for listing generally - // If decryption is needed for display, it should happen closer to the presentation layer or selectively return ProxyRepository.findAllProxies(); }; @@ -44,7 +39,6 @@ export const getAllProxies = async (): Promise => { * 根据 ID 获取单个代理 */ export const getProxyById = async (id: number): Promise => { - // Repository returns data with encrypted fields return ProxyRepository.findProxyById(id); }; @@ -52,7 +46,7 @@ export const getProxyById = async (id: number): Promise => { * 创建新代理 */ export const createProxy = async (input: CreateProxyInput): Promise => { - // 1. Validate input + // 1. 验证输入 if (!input.name || !input.type || !input.host || !input.port) { throw new Error('缺少必要的代理信息 (name, type, host, port)。'); } @@ -62,14 +56,13 @@ export const createProxy = async (input: CreateProxyInput): Promise = if (input.auth_method === 'key' && !input.private_key) { throw new Error('代理密钥认证方式需要提供 private_key。'); } - // Add more validation (port range, type check etc.) - // 2. Encrypt credentials if provided + // 2. 如果提供,则加密凭证 const encryptedPassword = input.password ? encrypt(input.password) : null; const encryptedPrivateKey = input.private_key ? encrypt(input.private_key) : null; const encryptedPassphrase = input.passphrase ? encrypt(input.passphrase) : null; - // 3. Prepare data for repository + // 3. 准备仓库数据 const proxyData: Omit = { name: input.name, type: input.type, @@ -82,10 +75,10 @@ export const createProxy = async (input: CreateProxyInput): Promise = encrypted_passphrase: encryptedPassphrase, }; - // 4. Create proxy record + // 4. 创建代理记录 const newProxyId = await ProxyRepository.createProxy(proxyData); - // 5. Fetch and return the newly created proxy + // 5. 获取并返回新创建的代理 const newProxy = await getProxyById(newProxyId); if (!newProxy) { throw new Error('创建代理后无法检索到该代理。'); @@ -97,62 +90,62 @@ export const createProxy = async (input: CreateProxyInput): Promise = * 更新代理信息 */ export const updateProxy = async (id: number, input: UpdateProxyInput): Promise => { - // 1. Fetch current proxy data to compare if needed (e.g., for auth method change logic) + // 1. 获取当前代理数据以进行比较(例如,用于认证方法更改逻辑) const currentProxy = await ProxyRepository.findProxyById(id); if (!currentProxy) { - return null; // Proxy not found + return null; // 未找到代理 } - // 2. Prepare data for update + // 2. 准备更新数据 const dataToUpdate: Partial> = {}; let needsCredentialUpdate = false; const newAuthMethod = input.auth_method || currentProxy.auth_method; - // Update standard fields + // 更新标准字段 if (input.name !== undefined) dataToUpdate.name = input.name; if (input.type !== undefined) dataToUpdate.type = input.type; if (input.host !== undefined) dataToUpdate.host = input.host; if (input.port !== undefined) dataToUpdate.port = input.port; - if (input.username !== undefined) dataToUpdate.username = input.username; // Allows clearing + if (input.username !== undefined) dataToUpdate.username = input.username; // 允许清除 - // Handle auth method change or credential update + // 处理认证方法更改或凭证更新 if (input.auth_method && input.auth_method !== currentProxy.auth_method) { dataToUpdate.auth_method = input.auth_method; needsCredentialUpdate = true; - // Encrypt new credentials based on the *new* auth_method + // 根据 *新* 认证方法加密新凭证 if (input.auth_method === 'password') { if (input.password === undefined) throw new Error('切换到密码认证时需要提供 password。'); dataToUpdate.encrypted_password = input.password ? encrypt(input.password) : null; - dataToUpdate.encrypted_private_key = null; // Clear old key info + dataToUpdate.encrypted_private_key = null; // 清除旧密钥信息 dataToUpdate.encrypted_passphrase = null; } else if (input.auth_method === 'key') { if (input.private_key === undefined) throw new Error('切换到密钥认证时需要提供 private_key。'); dataToUpdate.encrypted_private_key = input.private_key ? encrypt(input.private_key) : null; dataToUpdate.encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null; - dataToUpdate.encrypted_password = null; // Clear old password info - } else { // 'none' + dataToUpdate.encrypted_password = null; // 清除旧密码信息 + } else { // '无' dataToUpdate.encrypted_password = null; dataToUpdate.encrypted_private_key = null; dataToUpdate.encrypted_passphrase = null; } } else { - // Auth method unchanged, update credentials if provided for the current method + // 认证方法未更改,如果为当前方法提供了凭证,则更新凭证 if (newAuthMethod === 'password' && input.password !== undefined) { dataToUpdate.encrypted_password = input.password ? encrypt(input.password) : null; needsCredentialUpdate = true; } else if (newAuthMethod === 'key') { if (input.private_key !== undefined) { dataToUpdate.encrypted_private_key = input.private_key ? encrypt(input.private_key) : null; - dataToUpdate.encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null; // Update passphrase together + dataToUpdate.encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null; // 一起更新密码短语 needsCredentialUpdate = true; - } else if (input.passphrase !== undefined) { // Only passphrase updated + } else if (input.passphrase !== undefined) { // 仅更新密码短语 dataToUpdate.encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null; needsCredentialUpdate = true; } } } - // 3. Update proxy record if there are changes + // 3. 如果有更改,则更新代理记录 const hasChanges = Object.keys(dataToUpdate).length > 0; if (hasChanges) { const updated = await ProxyRepository.updateProxy(id, dataToUpdate); @@ -161,7 +154,7 @@ export const updateProxy = async (id: number, input: UpdateProxyInput): Promise< } } - // 4. Fetch and return the updated proxy + // 4. 获取并返回更新后的代理 return getProxyById(id); }; @@ -169,6 +162,5 @@ export const updateProxy = async (id: number, input: UpdateProxyInput): Promise< * 删除代理 */ export const deleteProxy = async (id: number): Promise => { - // Repository handles setting foreign keys to NULL in connections table return ProxyRepository.deleteProxy(id); }; diff --git a/packages/backend/src/services/settings.service.ts b/packages/backend/src/services/settings.service.ts index 425525c..72a9ca1 100644 --- a/packages/backend/src/services/settings.service.ts +++ b/packages/backend/src/services/settings.service.ts @@ -3,16 +3,16 @@ import { Setting, getSidebarConfig as getSidebarConfigFromRepo, setSidebarConfig as setSidebarConfigInRepo, - getCaptchaConfig as getCaptchaConfigFromRepo, // <-- Import CAPTCHA repo getter - setCaptchaConfig as setCaptchaConfigInRepo, // <-- Import CAPTCHA repo setter + getCaptchaConfig as getCaptchaConfigFromRepo, + setCaptchaConfig as setCaptchaConfigInRepo, } from '../repositories/settings.repository'; import { SidebarConfig, PaneName, UpdateSidebarConfigDto, - CaptchaSettings, // <-- Import CAPTCHA types - UpdateCaptchaSettingsDto, // <-- Import CAPTCHA types - CaptchaProvider, // <-- Import CAPTCHA types + CaptchaSettings, + UpdateCaptchaSettingsDto, + CaptchaProvider, } from '../types/settings.types'; // +++ 定义焦点切换完整配置接口 (与前端 store 保持一致) +++ @@ -128,8 +128,6 @@ export const settingsService = { Object.values(config.shortcuts).every((sc: any) => typeof sc === 'object' && sc !== null && (sc.shortcut === undefined || typeof sc.shortcut === 'string')) ) { console.log('[Service] Fetched and validated full focus switcher config:', JSON.stringify(config)); - // TODO: 可能需要进一步验证 sequence 中的 id 是否仍然有效 (存在于某个地方定义的可用 ID 列表) - // TODO: 可能需要进一步验证 shortcuts 中的 key 是否是有效的 ID return config as FocusSwitcherFullConfig; } else { console.warn('[Service] Invalid full focus switcher config format found in settings. Returning default.'); diff --git a/packages/backend/src/services/ssh.service.ts b/packages/backend/src/services/ssh.service.ts index 30137ad..8501e9e 100644 --- a/packages/backend/src/services/ssh.service.ts +++ b/packages/backend/src/services/ssh.service.ts @@ -3,7 +3,7 @@ import { SocksClient, SocksClientOptions } from 'socks'; import http from 'http'; import net from 'net'; import * as ConnectionRepository from '../repositories/connection.repository'; -import * as ProxyRepository from '../repositories/proxy.repository'; // 引入 ProxyRepository +import * as ProxyRepository from '../repositories/proxy.repository'; import { decrypt } from '../utils/crypto'; const CONNECT_TIMEOUT = 20000; // 连接超时时间 (毫秒) @@ -123,7 +123,6 @@ export const establishSshConnection = ( console.log(`SshService: SSH 连接到 ${connDetails.host}:${connDetails.port} (ID: ${connDetails.id}) 成功。`); sshClient.removeListener('error', errorHandler); // 成功后移除错误监听器 - // --- 新增:更新 last_connected_at --- try { const currentTimeSeconds = Math.floor(Date.now() / 1000); await ConnectionRepository.updateLastConnected(connDetails.id, currentTimeSeconds); @@ -132,7 +131,6 @@ export const establishSshConnection = ( // 更新失败不应阻止连接成功,但需要记录错误 console.error(`SshService: 更新连接 ${connDetails.id} 的 last_connected_at 失败:`, updateError); } - // --- 结束新增 --- resolve(sshClient); // 返回 Client 实例 }; @@ -354,11 +352,3 @@ export const testUnsavedConnection = async (connectionConfig: { } }; - -// --- 移除旧的函数 --- -// - connectAndOpenShell -// - sendInput -// - resizeTerminal -// - cleanupConnection -// - activeSessions Map -// - AuthenticatedWebSocket interface (如果仅在此文件使用) diff --git a/packages/backend/src/services/status-monitor.service.ts b/packages/backend/src/services/status-monitor.service.ts index dd0a515..93382b2 100644 --- a/packages/backend/src/services/status-monitor.service.ts +++ b/packages/backend/src/services/status-monitor.service.ts @@ -1,9 +1,9 @@ import { Client } from 'ssh2'; import { WebSocket } from 'ws'; -import { ClientState } from '../websocket'; // 导入统一的 ClientState -import { settingsService } from './settings.service'; // +++ 导入 settingsService +++ +import { ClientState } from '../websocket'; +import { settingsService } from './settings.service'; + -// 定义服务器状态的数据结构 (与前端 StatusMonitor.vue 匹配) interface ServerStatus { cpuPercent?: number; memPercent?: number; @@ -24,7 +24,7 @@ interface ServerStatus { timestamp: number; // 状态获取时间戳 } -// Interface for parsed network stats + interface NetworkStats { [interfaceName: string]: { rx_bytes: number; @@ -32,7 +32,7 @@ interface NetworkStats { } } -// const DEFAULT_POLLING_INTERVAL = 3000; // --- 移除常量,将从 settingsService 获取 --- + // 用于存储上一次的网络统计信息以计算速率 const previousNetStats = new Map(); @@ -46,16 +46,14 @@ export class StatusMonitorService { /** * 启动指定会话的状态轮询 * @param sessionId 会话 ID - * @param interval 轮询间隔 (毫秒),可选,默认为 DEFAULT_POLLING_INTERVAL // --- 参数移除 --- + * @param interval 轮询间隔 (毫秒),可选,默认为 DEFAULT_POLLING_INTERVAL */ - async startStatusPolling(sessionId: string): Promise { // --- 改为 async, 移除 interval 参数 --- + async startStatusPolling(sessionId: string): Promise { const state = this.clientStates.get(sessionId); if (!state || !state.sshClient) { - //console.warn(`[StatusMonitor] 无法为会话 ${sessionId} 启动状态轮询:状态无效或 SSH 客户端不存在。`); return; } if (state.statusIntervalId) { - //console.warn(`[StatusMonitor] 会话 ${sessionId} 的状态轮询已在运行中。`); return; } @@ -70,7 +68,6 @@ export class StatusMonitorService { intervalMs = 3000; // 出错时回退到 3 秒 } - //console.warn(`[StatusMonitor] 为会话 ${sessionId} 启动状态轮询,间隔 ${intervalMs}ms`); // 移除立即执行,让 setInterval 负责第一次调用,给连接更多准备时间 state.statusIntervalId = setInterval(() => { this.fetchAndSendServerStatus(sessionId); @@ -130,35 +127,31 @@ export class StatusMonitorService { const osReleaseOutput = await this.executeSshCommand(sshClient, 'cat /etc/os-release'); const nameMatch = osReleaseOutput.match(/^PRETTY_NAME="?([^"]+)"?/m); status.osName = nameMatch ? nameMatch[1] : (osReleaseOutput.match(/^NAME="?([^"]+)"?/m)?.[1] ?? 'Unknown'); - } catch (err) { /* 静默处理 */ } // --- 移除 console.warn --- + } catch (err) { } - // --- CPU Model (Try /proc/cpuinfo first, fallback to lscpu) --- try { let cpuModelOutput = ''; try { - // Try /proc/cpuinfo first, common on many systems including Alpine cpuModelOutput = await this.executeSshCommand(sshClient, "cat /proc/cpuinfo | grep 'model name' | head -n 1"); status.cpuModel = cpuModelOutput.match(/model name\s*:\s*(.*)/i)?.[1].trim(); } catch (procErr) { - // console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model from /proc/cpuinfo, trying lscpu...`, procErr); // --- 移除 console.warn --- - // Fallback to lscpu if /proc/cpuinfo fails + try { cpuModelOutput = await this.executeSshCommand(sshClient, "lscpu | grep 'Model name:'"); status.cpuModel = cpuModelOutput.match(/Model name:\s+(.*)/)?.[1].trim(); } catch (lscpuErr) { - // console.warn(`[StatusMonitor ${sessionId}] Failed to get CPU model from lscpu as well:`, lscpuErr); // --- 移除 console.warn --- + } } - // If still no model found after both attempts if (!status.cpuModel) { status.cpuModel = 'Unknown'; } - } catch (err) { // Catch any unexpected error during the process - // console.warn(`[StatusMonitor ${sessionId}] Error getting CPU model:`, err); // --- 移除 console.warn --- + } catch (err) { + status.cpuModel = 'Unknown'; } - // --- Memory and Swap --- + try { const freeOutput = await this.executeSshCommand(sshClient, 'free -m'); const lines = freeOutput.split('\n'); @@ -186,17 +179,15 @@ export class StatusMonitorService { } } } else { status.swapTotal = 0; status.swapUsed = 0; status.swapPercent = 0; } - } catch (err) { /* 静默处理 */ } // --- 移除 console.warn --- + } catch (err) { /* 静默处理 */ } - // --- Disk Usage (Root Partition, POSIX format for compatibility) --- + try { // 使用 df -kP / 获取 POSIX 标准格式输出,更稳定 const dfOutput = await this.executeSshCommand(sshClient, "df -kP /"); const lines = dfOutput.split('\n'); if (lines.length >= 2) { - const parts = lines[1].split(/\s+/); // 解析第二行 (数据行) - // POSIX 格式: Filesystem 1024-blocks Used Available Capacity Mounted on - // parts[1]=Total(KB), parts[2]=Used(KB), parts[4]=Capacity(%) + const parts = lines[1].split(/\s+/); if (parts.length >= 5) { const total = parseInt(parts[1], 10); const used = parseInt(parts[2], 10); @@ -207,9 +198,8 @@ export class StatusMonitorService { } } } - } catch (err) { /* 静默处理 */ } // --- 移除 console.warn --- + } catch (err) { /* 静默处理 */ } - // --- CPU Usage (Simplified from top) --- try { const topOutput = await this.executeSshCommand(sshClient, "top -bn1 | grep '%Cpu(s)' | head -n 1"); const idleMatch = topOutput.match(/(\d+\.?\d*)\s+id/); // Adjusted regex for float @@ -217,16 +207,15 @@ export class StatusMonitorService { const idlePercent = parseFloat(idleMatch[1]); status.cpuPercent = parseFloat((100 - idlePercent).toFixed(1)); } - } catch (err) { /* 静默处理 */ } // --- 移除 console.warn --- + } catch (err) { /* 静默处理 */ } // - // --- Load Average --- try { const uptimeOutput = await this.executeSshCommand(sshClient, 'uptime'); const match = uptimeOutput.match(/load average(?:s)?:\s*([\d.]+)[, ]?\s*([\d.]+)[, ]?\s*([\d.]+)/); if (match) status.loadAvg = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])]; - } catch (err) { /* 静默处理 */ } // --- 移除 console.warn --- + } catch (err) { /* 静默处理 */ } + - // --- Network Rates --- try { const currentStats = await this.parseProcNetDev(sshClient); if (currentStats) { @@ -238,18 +227,18 @@ export class StatusMonitorService { const currentTx = currentStats[defaultInterface].tx_bytes; const prevStats = previousNetStats.get(sessionId); - if (prevStats && prevStats.timestamp < timestamp) { // Ensure time has passed + if (prevStats && prevStats.timestamp < timestamp) { const timeDiffSeconds = (timestamp - prevStats.timestamp) / 1000; - if (timeDiffSeconds > 0.1) { // Avoid division by zero or tiny intervals + if (timeDiffSeconds > 0.1) { status.netRxRate = Math.max(0, Math.round((currentRx - prevStats.rx) / timeDiffSeconds)); status.netTxRate = Math.max(0, Math.round((currentTx - prevStats.tx) / timeDiffSeconds)); - } else { status.netRxRate = 0; status.netTxRate = 0; } // Rate is 0 if interval too small - } else { status.netRxRate = 0; status.netTxRate = 0; } // First run or no time diff + } else { status.netRxRate = 0; status.netTxRate = 0; } + } else { status.netRxRate = 0; status.netTxRate = 0; } previousNetStats.set(sessionId, { rx: currentRx, tx: currentTx, timestamp }); - } else { /* 静默处理 */ } // --- 移除 console.warn --- + } else { /* 静默处理 */ } } - } catch (err) { /* 静默处理 */ } // --- 移除 console.warn --- + } catch (err) { /* 静默处理 */ } } catch (error) { console.error(`[StatusMonitor ${sessionId}] General error fetching server status:`, error); @@ -270,7 +259,7 @@ export class StatusMonitorService { output = await this.executeSshCommand(sshClient, 'cat /proc/net/dev'); } catch (error) { // 如果命令失败,记录警告并返回 null - // console.warn("[StatusMonitor] Failed to execute 'cat /proc/net/dev':", error); // --- 移除 console.warn --- + return null; } // 如果命令成功,继续解析 @@ -279,18 +268,16 @@ export class StatusMonitorService { const stats: NetworkStats = {}; for (const line of lines) { const parts = line.trim().split(/:\s+|\s+/); - if (parts.length < 17) continue; // Need at least interface name + 16 stats + if (parts.length < 17) continue; const interfaceName = parts[0]; const rx_bytes = parseInt(parts[1], 10); - const tx_bytes = parseInt(parts[9], 10); // TX bytes is the 10th field (index 9) + const tx_bytes = parseInt(parts[9], 10); if (!isNaN(rx_bytes) && !isNaN(tx_bytes)) { stats[interfaceName] = { rx_bytes, tx_bytes }; } } return Object.keys(stats).length > 0 ? stats : null; } catch (parseError) { - // 如果解析失败,记录错误并返回 null - // console.error("[StatusMonitor] Error parsing /proc/net/dev output:", parseError); // --- 移除 console.error --- return null; } } @@ -307,11 +294,10 @@ export class StatusMonitorService { const interfaceName = output.trim(); if (interfaceName) return interfaceName; // 如果 ip route 没返回有效接口名,也尝试 fallback - // console.warn("[StatusMonitor] 'ip route' did not return a valid interface name. Falling back..."); // --- 移除 console.warn --- + } catch (error) { - // console.warn("[StatusMonitor] Failed to get default interface using 'ip route', falling back:", error); // --- 移除 console.warn --- - // Fallback: 尝试查找第一个非 lo 接口 + try { const netDevOutput = await this.executeSshCommand(sshClient, 'cat /proc/net/dev'); const lines = netDevOutput.split('\n').slice(2); @@ -322,13 +308,12 @@ export class StatusMonitorService { } } } catch (fallbackError) { - // console.error("[StatusMonitor] Failed to fallback to /proc/net/dev for interface:", fallbackError); // --- 移除 console.error --- + } - // Ensure null is returned if both primary and fallback fail within the outer catch + return null; } - // This part should ideally not be reached if the first try succeeded or the catch block returned. - // Adding a final return null for safety and to satisfy TS if logic paths are complex. + return null; } @@ -347,16 +332,10 @@ export class StatusMonitorService { return reject(new Error(`执行命令 '${command}' 失败: ${err.message}`)); } stream.on('close', (code: number, signal?: string) => { - // Don't reject on non-zero exit code, as some commands might return non-zero normally - // if (code !== 0) { - // //console.warn(`[StatusMonitor] Command '${command}' exited with code ${code}`); - // } resolve(output.trim()); }).on('data', (data: Buffer) => { output += data.toString('utf8'); }).stderr.on('data', (data: Buffer) => { - // --- 移除 console.warn --- - // console.warn(`[StatusMonitor] Command '${command}' stderr: ${data.toString('utf8').trim()}`); }); }); }); diff --git a/packages/backend/src/services/tag.service.ts b/packages/backend/src/services/tag.service.ts index 36c4792..93b41d0 100644 --- a/packages/backend/src/services/tag.service.ts +++ b/packages/backend/src/services/tag.service.ts @@ -21,16 +21,15 @@ export const getTagById = async (id: number): Promise => { * 创建新标签 */ export const createTag = async (name: string): Promise => { - // 1. Validate input + if (!name || name.trim().length === 0) { throw new Error('标签名称不能为空。'); } const trimmedName = name.trim(); - // 2. Create tag record + try { const newTagId = await TagRepository.createTag(trimmedName); - // 3. Fetch and return the newly created tag const newTag = await getTagById(newTagId); if (!newTag) { throw new Error('创建标签后无法检索到该标签。'); @@ -40,7 +39,7 @@ export const createTag = async (name: string): Promise => { if (error.message.includes('UNIQUE constraint failed')) { throw new Error(`创建标签失败:标签名称 "${trimmedName}" 已存在。`); } - throw error; // Re-throw other errors + throw error; } }; @@ -48,25 +47,25 @@ export const createTag = async (name: string): Promise => { * 更新标签名称 */ export const updateTag = async (id: number, name: string): Promise => { - // 1. Validate input + if (!name || name.trim().length === 0) { throw new Error('标签名称不能为空。'); } const trimmedName = name.trim(); - // 2. Update tag record + try { const updated = await TagRepository.updateTag(id, trimmedName); if (!updated) { - return null; // Tag not found or not updated + return null; } - // 3. Fetch and return the updated tag + return getTagById(id); } catch (error: any) { if (error.message.includes('UNIQUE constraint failed')) { throw new Error(`更新标签失败:标签名称 "${trimmedName}" 已存在。`); } - throw error; // Re-throw other errors + throw error; } }; @@ -74,6 +73,5 @@ export const updateTag = async (id: number, name: string): Promise => { - // Repository handles cascading deletes in connection_tags return TagRepository.deleteTag(id); }; diff --git a/packages/backend/src/services/terminal-theme.service.ts b/packages/backend/src/services/terminal-theme.service.ts index 8160747..088d98d 100644 --- a/packages/backend/src/services/terminal-theme.service.ts +++ b/packages/backend/src/services/terminal-theme.service.ts @@ -1,6 +1,5 @@ import * as terminalThemeRepository from '../repositories/terminal-theme.repository'; import { TerminalTheme, CreateTerminalThemeDto, UpdateTerminalThemeDto } from '../types/terminal-theme.types'; -// import { validate } from 'class-validator'; // 移除导入 import type { ITheme } from 'xterm'; /** @@ -85,4 +84,3 @@ export const importTheme = async (themeData: ITheme, name: string): Promise = {}; for (const key in settingsToUpdate) { @@ -74,12 +73,12 @@ export const settingsController = { */ async getFocusSwitcherSequence(req: Request, res: Response): Promise { try { - console.log('[Controller] Received request to get focus switcher sequence.'); + console.log('[控制器] 收到获取焦点切换顺序的请求。'); const sequence = await settingsService.getFocusSwitcherSequence(); - console.log('[Controller] Sending focus switcher sequence to client:', JSON.stringify(sequence)); + console.log('[控制器] 向客户端发送焦点切换顺序:', JSON.stringify(sequence)); res.json(sequence); } catch (error: any) { - console.error('[Controller] 获取焦点切换顺序时出错:', error); + console.error('[控制器] 获取焦点切换顺序时出错:', error); res.status(500).json({ message: '获取焦点切换顺序失败', error: error.message }); } }, @@ -88,11 +87,11 @@ export const settingsController = { * 设置焦点切换顺序 */ async setFocusSwitcherSequence(req: Request, res: Response): Promise { - console.log('[Controller] Received request to set focus switcher sequence.'); + console.log('[控制器] 收到设置焦点切换顺序的请求。'); try { // +++ 修改:获取请求体并验证其是否符合 FocusSwitcherFullConfig 结构 +++ const fullConfig = req.body; - console.log('[Controller] Request body fullConfig:', JSON.stringify(fullConfig)); + console.log('[控制器] 请求体 fullConfig:', JSON.stringify(fullConfig)); // +++ 验证 FocusSwitcherFullConfig 结构 +++ if ( @@ -101,23 +100,22 @@ export const settingsController = { typeof fullConfig.shortcuts === 'object' && fullConfig.shortcuts !== null && Object.values(fullConfig.shortcuts).every((sc: any) => typeof sc === 'object' && sc !== null && (sc.shortcut === undefined || typeof sc.shortcut === 'string'))) ) { - console.warn('[Controller] Invalid full focus config format received:', fullConfig); + console.warn('[控制器] 收到无效的完整焦点配置格式:', fullConfig); res.status(400).json({ message: '无效的请求体,必须是包含 sequence (string[]) 和 shortcuts (Record) 的对象' }); return; } - console.log('[Controller] Calling settingsService.setFocusSwitcherSequence with validated full config...'); + console.log('[控制器] 使用验证后的完整配置调用 settingsService.setFocusSwitcherSequence...'); // +++ 传递验证后的 fullConfig 给服务层 +++ await settingsService.setFocusSwitcherSequence(fullConfig); - console.log('[Controller] settingsService.setFocusSwitcherSequence completed successfully.'); + console.log('[控制器] settingsService.setFocusSwitcherSequence 成功完成。'); - console.log('[Controller] Logging audit action: FOCUS_SWITCHER_SEQUENCE_UPDATED'); // Keep console log for now if needed - // auditLogService.logAction('FOCUS_SWITCHER_SEQUENCE_UPDATED', { config: fullConfig }); // Removed specific log - - console.log('[Controller] Sending success response.'); + console.log('[控制器] 记录审计操作: FOCUS_SWITCHER_SEQUENCE_UPDATED'); + + console.log('[控制器] 发送成功响应。'); res.status(200).json({ message: '焦点切换顺序已成功更新' }); } catch (error: any) { - console.error('[Controller] 设置焦点切换顺序时出错:', error); + console.error('[控制器] 设置焦点切换顺序时出错:', error); if (error.message === 'Invalid sequence format provided.') { res.status(400).json({ message: '设置焦点切换顺序失败: 无效的格式', error: error.message }); } else { @@ -131,12 +129,12 @@ export const settingsController = { */ async getNavBarVisibility(req: Request, res: Response): Promise { try { - console.log('[Controller] Received request to get nav bar visibility.'); + console.log('[控制器] 收到获取导航栏可见性的请求。'); const isVisible = await settingsService.getNavBarVisibility(); - console.log(`[Controller] Sending nav bar visibility to client: ${isVisible}`); + console.log(`[控制器] 向客户端发送导航栏可见性: ${isVisible}`); res.json({ visible: isVisible }); } catch (error: any) { - console.error('[Controller] 获取导航栏可见性时出错:', error); + console.error('[控制器] 获取导航栏可见性时出错:', error); res.status(500).json({ message: '获取导航栏可见性失败', error: error.message }); } }, @@ -145,27 +143,26 @@ export const settingsController = { * 设置导航栏可见性 */ async setNavBarVisibility(req: Request, res: Response): Promise { - console.log('[Controller] Received request to set nav bar visibility.'); + console.log('[控制器] 收到设置导航栏可见性的请求。'); try { const { visible } = req.body; - console.log('[Controller] Request body visible:', visible); + console.log('[控制器] 请求体 visible:', visible); if (typeof visible !== 'boolean') { - console.warn('[Controller] Invalid visible format received:', visible); + console.warn('[控制器] 收到无效的 visible 格式:', visible); res.status(400).json({ message: '无效的请求体,"visible" 必须是一个布尔值' }); return; } - console.log('[Controller] Calling settingsService.setNavBarVisibility...'); + console.log('[控制器] 调用 settingsService.setNavBarVisibility...'); await settingsService.setNavBarVisibility(visible); - console.log('[Controller] settingsService.setNavBarVisibility completed successfully.'); + console.log('[控制器] settingsService.setNavBarVisibility 成功完成。'); - // auditLogService.logAction('NAV_BAR_VISIBILITY_UPDATED', { visible }); - console.log('[Controller] Sending success response.'); + console.log('[控制器] 发送成功响应。'); res.status(200).json({ message: '导航栏可见性已成功更新' }); } catch (error: any) { - console.error('[Controller] 设置导航栏可见性时出错:', error); + console.error('[控制器] 设置导航栏可见性时出错:', error); res.status(500).json({ message: '设置导航栏可见性失败', error: error.message }); } }, @@ -175,23 +172,23 @@ export const settingsController = { */ async getLayoutTree(req: Request, res: Response): Promise { try { - console.log('[Controller] Received request to get layout tree.'); + console.log('[控制器] 收到获取布局树的请求。'); const layoutJson = await settingsService.getLayoutTree(); if (layoutJson) { try { const layout = JSON.parse(layoutJson); - console.log('[Controller] Sending layout tree to client.'); + console.log('[控制器] 向客户端发送布局树。'); res.json(layout); } catch (parseError) { - console.error('[Controller] Failed to parse layout tree JSON from DB:', parseError); + console.error('[控制器] 从数据库解析布局树 JSON 失败:', parseError); res.status(500).json({ message: '获取布局树失败:存储的数据格式无效' }); } } else { - console.log('[Controller] No layout tree found in settings, sending null.'); + console.log('[控制器] 在设置中未找到布局树,发送 null。'); res.json(null); } } catch (error: any) { - console.error('[Controller] 获取布局树时出错:', error); + console.error('[控制器] 获取布局树时出错:', error); res.status(500).json({ message: '获取布局树失败', error: error.message }); } }, @@ -200,28 +197,28 @@ export const settingsController = { * 设置布局树 */ async setLayoutTree(req: Request, res: Response): Promise { - console.log('[Controller] Received request to set layout tree.'); + console.log('[控制器] 收到设置布局树的请求。'); try { const layoutTree = req.body; if (typeof layoutTree !== 'object' || layoutTree === null) { - console.warn('[Controller] Invalid layout tree format received (not an object):', layoutTree); + console.warn('[控制器] 收到无效的布局树格式 (非对象):', layoutTree); res.status(400).json({ message: '无效的请求体,应为 JSON 对象格式的布局树' }); return; } const layoutJson = JSON.stringify(layoutTree); - console.log('[Controller] Calling settingsService.setLayoutTree...'); + console.log('[控制器] 调用 settingsService.setLayoutTree...'); await settingsService.setLayoutTree(layoutJson); - console.log('[Controller] settingsService.setLayoutTree completed successfully.'); + console.log('[控制器] settingsService.setLayoutTree 成功完成。'); // auditLogService.logAction('LAYOUT_TREE_UPDATED'); - console.log('[Controller] Sending success response.'); + console.log('[控制器] 发送成功响应。'); res.status(200).json({ message: '布局树已成功更新' }); } catch (error: any) { - console.error('[Controller] 设置布局树时出错:', error); + console.error('[控制器] 设置布局树时出错:', error); if (error.message === 'Invalid layout tree JSON format.') { res.status(400).json({ message: '设置布局树失败: 无效的 JSON 格式', error: error.message }); } else { @@ -256,7 +253,6 @@ export const settingsController = { return; } await ipBlacklistService.removeFromBlacklist(ipToDelete); - // auditLogService.logAction('IP_BLACKLIST_REMOVED', { ip: ipToDelete }); res.status(200).json({ message: `IP 地址 ${ipToDelete} 已从黑名单中移除` }); } catch (error: any) { console.error(`从 IP 黑名单删除 ${req.params.ip} 时出错:`, error); @@ -269,12 +265,12 @@ export const settingsController = { */ async getAutoCopyOnSelect(req: Request, res: Response): Promise { try { - console.log('[Controller] Received request to get auto copy on select setting.'); + console.log('[控制器] 收到获取“选中时自动复制”设置的请求。'); const isEnabled = await settingsService.getAutoCopyOnSelect(); - console.log(`[Controller] Sending auto copy on select setting to client: ${isEnabled}`); + console.log(`[控制器] 向客户端发送“选中时自动复制”设置: ${isEnabled}`); res.json({ enabled: isEnabled }); } catch (error: any) { - console.error('[Controller] 获取终端选中自动复制设置时出错:', error); + console.error('[控制器] 获取终端选中自动复制设置时出错:', error); res.status(500).json({ message: '获取终端选中自动复制设置失败', error: error.message }); } }, // *** 确保这里有逗号 *** @@ -283,44 +279,42 @@ export const settingsController = { * 设置终端选中自动复制 */ async setAutoCopyOnSelect(req: Request, res: Response): Promise { - console.log('[Controller] Received request to set auto copy on select setting.'); + console.log('[控制器] 收到设置“选中时自动复制”设置的请求。'); try { const { enabled } = req.body; - console.log('[Controller] Request body enabled:', enabled); + console.log('[控制器] 请求体 enabled:', enabled); if (typeof enabled !== 'boolean') { - console.warn('[Controller] Invalid enabled format received:', enabled); + console.warn('[控制器] 收到无效的 enabled 格式:', enabled); res.status(400).json({ message: '无效的请求体,"enabled" 必须是一个布尔值' }); return; } - console.log('[Controller] Calling settingsService.setAutoCopyOnSelect...'); + console.log('[控制器] 调用 settingsService.setAutoCopyOnSelect...'); await settingsService.setAutoCopyOnSelect(enabled); - console.log('[Controller] settingsService.setAutoCopyOnSelect completed successfully.'); + console.log('[控制器] settingsService.setAutoCopyOnSelect 成功完成。'); - // auditLogService.logAction('AUTO_COPY_ON_SELECT_UPDATED', { enabled }); // 可选:添加审计日志 - console.log('[Controller] Sending success response.'); + console.log('[控制器] 发送成功响应。'); res.status(200).json({ message: '终端选中自动复制设置已成功更新' }); } catch (error: any) { - console.error('[Controller] 设置终端选中自动复制时出错:', error); + console.error('[控制器] 设置终端选中自动复制时出错:', error); res.status(500).json({ message: '设置终端选中自动复制失败', error: error.message }); } - }, // *** 确保这里有逗号 *** + }, - // --- Sidebar Config Controller Methods --- /** * 获取侧栏配置 */ async getSidebarConfig(req: Request, res: Response): Promise { try { - console.log('[Controller] Received request to get sidebar config.'); + console.log('[控制器] 收到获取侧边栏配置的请求。'); const config = await settingsService.getSidebarConfig(); - console.log('[Controller] Sending sidebar config to client:', config); + console.log('[控制器] 向客户端发送侧边栏配置:', config); res.json(config); } catch (error: any) { - console.error('[Controller] 获取侧栏配置时出错:', error); + console.error('[控制器] 获取侧栏配置时出错:', error); res.status(500).json({ message: '获取侧栏配置失败', error: error.message }); } }, @@ -329,29 +323,28 @@ export const settingsController = { * 设置侧栏配置 */ async setSidebarConfig(req: Request, res: Response): Promise { - console.log('[Controller] Received request to set sidebar config.'); + console.log('[控制器] 收到设置侧边栏配置的请求。'); try { const configDto: UpdateSidebarConfigDto = req.body; - console.log('[Controller] Request body:', configDto); + console.log('[控制器] 请求体:', 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); + console.warn('[控制器] 收到无效的侧边栏配置格式:', configDto); res.status(400).json({ message: '无效的请求体,应为包含 left 和 right 数组的 JSON 对象' }); return; } - console.log('[Controller] Calling settingsService.setSidebarConfig...'); + console.log('[控制器] 调用 settingsService.setSidebarConfig...'); await settingsService.setSidebarConfig(configDto); - console.log('[Controller] settingsService.setSidebarConfig completed successfully.'); + console.log('[控制器] settingsService.setSidebarConfig 成功完成。'); - // auditLogService.logAction('SIDEBAR_CONFIG_UPDATED'); // Optional: Add audit log - console.log('[Controller] Sending success response.'); + console.log('[控制器] 发送成功响应。'); res.status(200).json({ message: '侧栏配置已成功更新' }); } catch (error: any) { - console.error('[Controller] 设置侧栏配置时出错:', error); + console.error('[控制器] 设置侧栏配置时出错:', error); // Handle specific validation errors from the service if (error.message.includes('无效的面板名称') || error.message.includes('无效的侧栏配置格式')) { res.status(400).json({ message: `设置侧栏配置失败: ${error.message}` }); @@ -359,19 +352,16 @@ export const settingsController = { res.status(500).json({ message: '设置侧栏配置失败', error: error.message }); } } -}, // <-- Add comma here - -// --- CAPTCHA Settings Controller Methods --- +}, /** * 获取公共 CAPTCHA 配置 (不含密钥) */ async getCaptchaConfig(req: Request, res: Response): Promise { try { - console.log('[Controller] Received request to get CAPTCHA config.'); + console.log('[控制器] 收到获取 CAPTCHA 配置的请求。'); const fullConfig = await settingsService.getCaptchaConfig(); - // *** IMPORTANT: Filter out secret keys before sending to frontend *** const publicConfig = { enabled: fullConfig.enabled, provider: fullConfig.provider, @@ -379,10 +369,10 @@ async getCaptchaConfig(req: Request, res: Response): Promise { recaptchaSiteKey: fullConfig.recaptchaSiteKey, }; - console.log('[Controller] Sending public CAPTCHA config to client:', publicConfig); + console.log('[控制器] 向客户端发送公共 CAPTCHA 配置:', publicConfig); res.json(publicConfig); } catch (error: any) { - console.error('[Controller] 获取 CAPTCHA 配置时出错:', error); + console.error('[控制器] 获取 CAPTCHA 配置时出错:', error); res.status(500).json({ message: '获取 CAPTCHA 配置失败', error: error.message }); } }, @@ -391,37 +381,35 @@ async getCaptchaConfig(req: Request, res: Response): Promise { * 设置 CAPTCHA 配置 */ async setCaptchaConfig(req: Request, res: Response): Promise { - console.log('[Controller] Received request to set CAPTCHA config.'); + console.log('[控制器] 收到设置 CAPTCHA 配置的请求。'); try { const configDto: UpdateCaptchaSettingsDto = req.body; - // Mask secrets immediately if logging the DTO - console.log('[Controller] Request body (DTO, secrets masked):', { ...configDto, hcaptchaSecretKey: '***', recaptchaSecretKey: '***' }); + console.log('[控制器] 请求体 (DTO, 密钥已屏蔽):', { ...configDto, hcaptchaSecretKey: '***', recaptchaSecretKey: '***' }); - // --- DTO Validation (Basic) --- if (!configDto || typeof configDto !== 'object') { - console.warn('[Controller] Invalid CAPTCHA config format received (not an object):', configDto); + console.warn('[控制器] 收到无效的 CAPTCHA 配置格式 (非对象):', configDto); res.status(400).json({ message: '无效的请求体,应为 JSON 对象' }); return; } - // More specific validation happens in the service layer - console.log('[Controller] Calling settingsService.setCaptchaConfig...'); + + console.log('[控制器] 调用 settingsService.setCaptchaConfig...'); await settingsService.setCaptchaConfig(configDto); - console.log('[Controller] settingsService.setCaptchaConfig completed successfully.'); + console.log('[控制器] settingsService.setCaptchaConfig 成功完成。'); - auditLogService.logAction('CAPTCHA_SETTINGS_UPDATED'); // Add audit log + auditLogService.logAction('CAPTCHA_SETTINGS_UPDATED'); - console.log('[Controller] Sending success response.'); + console.log('[控制器] 发送成功响应。'); res.status(200).json({ message: 'CAPTCHA 配置已成功更新' }); } catch (error: any) { - console.error('[Controller] 设置 CAPTCHA 配置时出错:', error); + console.error('[控制器] 设置 CAPTCHA 配置时出错:', error); // Handle specific validation errors from the service - if (error.message.includes('无效的') || error.message.includes('必须是')) { // Basic check for validation errors + if (error.message.includes('无效的') || error.message.includes('必须是')) { 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 +}; diff --git a/packages/backend/src/settings/settings.routes.ts b/packages/backend/src/settings/settings.routes.ts index 1498e26..2527f8a 100644 --- a/packages/backend/src/settings/settings.routes.ts +++ b/packages/backend/src/settings/settings.routes.ts @@ -1,10 +1,10 @@ import express from 'express'; import { settingsController } from './settings.controller'; -import { isAuthenticated } from '../auth/auth.middleware'; // 导入认证中间件 +import { isAuthenticated } from '../auth/auth.middleware'; const router = express.Router(); -// +++ 新增:CAPTCHA 配置路由 (公开获取) +++ + // GET /api/v1/settings/captcha - 获取公共 CAPTCHA 配置 (不含密钥) router.get('/captcha', settingsController.getCaptchaConfig); diff --git a/packages/backend/src/sftp/sftp.controller.ts b/packages/backend/src/sftp/sftp.controller.ts index 83558c7..7d8b791 100644 --- a/packages/backend/src/sftp/sftp.controller.ts +++ b/packages/backend/src/sftp/sftp.controller.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; -import path from 'path'; // 需要 path 用于处理文件名 -import { clientStates } from '../websocket'; // Import the exported clientStates map +import path from 'path'; +import { clientStates } from '../websocket'; /** * 处理文件下载请求 (GET /api/v1/sftp/download) @@ -25,13 +25,11 @@ export const downloadFile = async (req: Request, res: Response): Promise = // 查找与当前用户会话关联的活动 WebSocket 连接和 SFTP 会话 let userSftpSession = null; // 注意:这种查找方式效率不高,实际应用中可能需要更优化的结构来按 userId 查找连接 - // TODO: Refactor this to use SftpService instead of directly accessing clientStates - // This direct access is not ideal and couples the controller to websocket internals. for (const [sessionId, state] of clientStates.entries()) { - const ws = state.ws; // Get the WebSocket instance from the state - const connData = state; // Use the entire state object + const ws = state.ws; + const connData = state; // 假设 AuthenticatedWebSocket 上存储了 userId - if (ws.userId === userId && connData.sftp) { // Access userId directly from AuthenticatedWebSocket + if (ws.userId === userId && connData.sftp) { // 这里简单地取第一个找到的匹配连接,没有处理 connectionId 的匹配 // TODO: 需要一种方式将 HTTP 请求与特定的 WebSocket/SSH/SFTP 会话关联起来 // 临时方案:假设一个用户只有一个活动的 SSH/SFTP 会话 @@ -87,7 +85,7 @@ export const downloadFile = async (req: Request, res: Response): Promise = // 监听响应对象的 close 事件,确保流被正确关闭 (虽然 pipe 通常会处理) res.on('close', () => { console.log(`SFTP 下载流关闭 (用户 ${userId}, 路径 ${remotePath})`); - // readStream.destroy(); // 可选:显式销毁流 + }); console.log(`SFTP 开始下载 (用户 ${userId}, 路径 ${remotePath})`); @@ -104,5 +102,4 @@ export const downloadFile = async (req: Request, res: Response): Promise = } }; -// 其他 SFTP 控制器函数 (例如上传) -// export const uploadFile = ... + diff --git a/packages/backend/src/sftp/sftp.routes.ts b/packages/backend/src/sftp/sftp.routes.ts index 391a249..89243ff 100644 --- a/packages/backend/src/sftp/sftp.routes.ts +++ b/packages/backend/src/sftp/sftp.routes.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; import { isAuthenticated } from '../auth/auth.middleware'; -import { downloadFile } from './sftp.controller'; // 稍后创建 +import { downloadFile } from './sftp.controller'; const router = Router(); @@ -10,6 +10,5 @@ router.use(isAuthenticated); // GET /api/v1/sftp/download?connectionId=...&remotePath=... router.get('/download', downloadFile); -// 未来可以添加其他 SFTP 相关 REST API (如果需要,例如上传的大文件断点续传初始化) export default router; diff --git a/packages/backend/src/tags/tags.controller.ts b/packages/backend/src/tags/tags.controller.ts index 374d198..aa094c9 100644 --- a/packages/backend/src/tags/tags.controller.ts +++ b/packages/backend/src/tags/tags.controller.ts @@ -1,8 +1,8 @@ import { Request, Response } from 'express'; import * as TagService from '../services/tag.service'; -import { AuditLogService } from '../services/audit.service'; // 引入 AuditLogService +import { AuditLogService } from '../services/audit.service'; -const auditLogService = new AuditLogService(); // 实例化 AuditLogService +const auditLogService = new AuditLogService(); /** * 创建新标签 (POST /api/v1/tags) @@ -23,7 +23,7 @@ export const createTag = async (req: Request, res: Response): Promise => { } catch (error: any) { console.error('Controller: 创建标签时发生错误:', error); if (error.message.includes('已存在')) { - res.status(409).json({ message: error.message }); // Conflict + res.status(409).json({ message: error.message }); } else { res.status(500).json({ message: error.message || '创建标签时发生内部服务器错误。' }); } @@ -95,7 +95,7 @@ export const updateTag = async (req: Request, res: Response): Promise => { } catch (error: any) { console.error(`Controller: 更新标签 ${tagId} 时发生错误:`, error); if (error.message.includes('已存在')) { - res.status(409).json({ message: error.message }); // Conflict + res.status(409).json({ message: error.message }); } else if (error.message.includes('不能为空')) { res.status(400).json({ message: error.message }); } diff --git a/packages/backend/src/tags/tags.routes.ts b/packages/backend/src/tags/tags.routes.ts index d76ff0f..1dfd50e 100644 --- a/packages/backend/src/tags/tags.routes.ts +++ b/packages/backend/src/tags/tags.routes.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { isAuthenticated } from '../auth/auth.middleware'; // 引入认证中间件 +import { isAuthenticated } from '../auth/auth.middleware'; import { createTag, getTags, diff --git a/packages/backend/src/terminal-themes/terminal-theme.controller.ts b/packages/backend/src/terminal-themes/terminal-theme.controller.ts index ba3e25f..f2b7316 100644 --- a/packages/backend/src/terminal-themes/terminal-theme.controller.ts +++ b/packages/backend/src/terminal-themes/terminal-theme.controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import * as terminalThemeService from '../services/terminal-theme.service'; import { CreateTerminalThemeDto, UpdateTerminalThemeDto } from '../types/terminal-theme.types'; import type { ITheme } from 'xterm'; -import multer from 'multer'; // 用于处理文件上传 +import multer from 'multer'; import fs from 'fs'; import path from 'path'; diff --git a/packages/backend/src/terminal-themes/terminal-theme.routes.ts b/packages/backend/src/terminal-themes/terminal-theme.routes.ts index cd8ac42..ac04c94 100644 --- a/packages/backend/src/terminal-themes/terminal-theme.routes.ts +++ b/packages/backend/src/terminal-themes/terminal-theme.routes.ts @@ -1,6 +1,6 @@ import express from 'express'; import * as themeController from './terminal-theme.controller'; -import { isAuthenticated } from '../auth/auth.middleware'; // 修正导入名称 +import { isAuthenticated } from '../auth/auth.middleware'; const router = express.Router(); diff --git a/packages/backend/src/types/audit.types.ts b/packages/backend/src/types/audit.types.ts index 542ecd5..95c900e 100644 --- a/packages/backend/src/types/audit.types.ts +++ b/packages/backend/src/types/audit.types.ts @@ -8,13 +8,13 @@ export type AuditLogActionType = | '2FA_ENABLED' | '2FA_DISABLED' | 'PASSKEY_REGISTERED' - | 'PASSKEY_DELETED' // Assuming deletion is possible later + | 'PASSKEY_DELETED' // Connections | 'CONNECTION_CREATED' | 'CONNECTION_UPDATED' | 'CONNECTION_DELETED' - | 'CONNECTION_TESTED' // Maybe log test attempts? + | 'CONNECTION_TESTED' | 'CONNECTIONS_IMPORTED' | 'CONNECTIONS_EXPORTED' @@ -31,8 +31,8 @@ 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 +| 'CAPTCHA_SETTINGS_UPDATED' + // Notifications | 'NOTIFICATION_SETTING_CREATED' @@ -53,9 +53,9 @@ export type AuditLogActionType = // System/Error | 'SERVER_STARTED' - | 'SERVER_ERROR' // Log significant backend errors + | 'SERVER_ERROR' | 'DATABASE_MIGRATION' - | 'ADMIN_SETUP_COMPLETE'; // *** 新增:初始管理员设置完成 *** + | 'ADMIN_SETUP_COMPLETE'; // 审计日志条目的结构 (从数据库读取时) export interface AuditLogEntry { diff --git a/packages/backend/src/types/connection.types.ts b/packages/backend/src/types/connection.types.ts index c3351bb..8e2b277 100644 --- a/packages/backend/src/types/connection.types.ts +++ b/packages/backend/src/types/connection.types.ts @@ -1,8 +1,8 @@ -// Centralized types for Connection feature + export interface ConnectionBase { id: number; - name: string | null; // Allow name to be null + name: string | null; host: string; port: number; username: string; @@ -17,22 +17,21 @@ export interface ConnectionWithTags extends ConnectionBase { tag_ids: number[]; } -// Input type for creating a connection (from controller) + export interface CreateConnectionInput { - name?: string; // Name is now optional + name?: string; host: string; - port?: number; // Optional, defaults in service/repo + port?: number; username: string; auth_method: 'password' | 'key'; - password?: string; // Optional depending on auth_method - private_key?: string; // Optional depending on auth_method - passphrase?: string; // Optional for key auth + password?: string; + private_key?: string; + passphrase?: string; proxy_id?: number | null; tag_ids?: number[]; } -// Input type for updating a connection (from controller) -// All fields are optional except potentially auth_method related ones + export interface UpdateConnectionInput { name?: string; host?: string; @@ -41,13 +40,12 @@ export interface UpdateConnectionInput { auth_method?: 'password' | 'key'; password?: string; private_key?: string; - passphrase?: string; // Use undefined to signal no change, null/empty string to clear + passphrase?: string; proxy_id?: number | null; tag_ids?: number[]; } -// Type used within the repository (includes encrypted fields) -// This might stay in the repository or be defined here if needed elsewhere + export interface FullConnectionData { id: number; name: string | null; diff --git a/packages/backend/src/types/notification.types.ts b/packages/backend/src/types/notification.types.ts index e1a53c0..5edb03f 100644 --- a/packages/backend/src/types/notification.types.ts +++ b/packages/backend/src/types/notification.types.ts @@ -8,15 +8,11 @@ export type NotificationEvent = | 'CONNECTIONS_IMPORTED' | 'CONNECTIONS_EXPORTED' | 'PROXY_CREATED' | 'PROXY_UPDATED' | 'PROXY_DELETED' | 'TAG_CREATED' | 'TAG_UPDATED' | 'TAG_DELETED' - | 'SETTINGS_UPDATED' | 'IP_WHITELIST_UPDATED' | 'IP_BLOCKED' // Added IP_BLOCKED event + | 'SETTINGS_UPDATED' | 'IP_WHITELIST_UPDATED' | 'IP_BLOCKED' | 'NOTIFICATION_SETTING_CREATED' | 'NOTIFICATION_SETTING_UPDATED' | 'NOTIFICATION_SETTING_DELETED' | 'SFTP_ACTION' - // SSH Actions | 'SSH_CONNECT_SUCCESS' | 'SSH_CONNECT_FAILURE' | 'SSH_SHELL_FAILURE' - // System/Error | 'SERVER_STARTED' | 'SERVER_ERROR' | 'DATABASE_MIGRATION' | 'ADMIN_SETUP_COMPLETE'; - // Settings (Specific) - Keep aligned with AuditLogActionType - // Note: IP_BLACKLISTED was in NotificationEvent but not AuditLogActionType, removed for consistency based on user request export interface WebhookConfig { url: string; diff --git a/packages/backend/src/websocket.ts b/packages/backend/src/websocket.ts index bce2b81..17a220c 100644 --- a/packages/backend/src/websocket.ts +++ b/packages/backend/src/websocket.ts @@ -2,16 +2,14 @@ import WebSocket, { WebSocketServer } from 'ws'; import http from 'http'; import { Request, RequestHandler } from 'express'; import { Client, ClientChannel } from 'ssh2'; -import { v4 as uuidv4 } from 'uuid'; // 用于生成唯一的会话 ID -import { getDbInstance } from './database/connection'; // Updated import path, use getDbInstance -import { decrypt } from './utils/crypto'; +import { v4 as uuidv4 } from 'uuid'; +import { getDbInstance } from './database/connection'; import { SftpService } from './services/sftp.service'; import { StatusMonitorService } from './services/status-monitor.service'; import * as SshService from './services/ssh.service'; -import { DockerService } from './services/docker.service'; // 导入 DockerService -import { AuditLogService } from './services/audit.service'; // 导入 AuditLogService -import { AuditLogActionType } from './types/audit.types'; // 导入 AuditLogActionType -import { settingsService } from './services/settings.service'; // +++ 修正导入路径 +++ +import { DockerService } from './services/docker.service'; +import { AuditLogService } from './services/audit.service'; +import { settingsService } from './services/settings.service'; // 扩展 WebSocket 类型以包含会话 ID interface AuthenticatedWebSocket extends WebSocket { @@ -35,15 +33,14 @@ export interface ClientState { // 导出以便 Service 可以导入 ipAddress?: string; // 添加 IP 地址字段 } -// --- Interfaces (保持与前端一致) --- -// --- FIX: Move PortInfo definition before its usage --- + interface PortInfo { IP?: string; PrivatePort: number; PublicPort?: number; Type: 'tcp' | 'udp' | string; } -// --- End FIX --- + // --- Docker Interfaces (Ensure this matches frontend and DockerService) --- // Stats 接口 @@ -72,16 +69,14 @@ interface DockerContainer { Labels: Record; stats?: DockerStats | null; // 可选的 stats 字段 } -// --- End Docker Interfaces --- // --- 新增:解析 Ports 字符串的辅助函数 --- -function parsePortsString(portsString: string | undefined | null): PortInfo[] { // Now PortInfo is defined +function parsePortsString(portsString: string | undefined | null): PortInfo[] { if (!portsString) { return []; } const ports: PortInfo[] = []; // Now PortInfo is defined - // 示例格式: "0.0.0.0:8080->80/tcp, :::8080->80/tcp", "127.0.0.1:5432->5432/tcp", "6379/tcp" const entries = portsString.split(', '); for (const entry of entries) { @@ -89,17 +84,16 @@ function parsePortsString(portsString: string | undefined | null): PortInfo[] { let publicPart = ''; let privatePart = ''; - if (parts.length === 2) { // Format like "IP:PublicPort->PrivatePort/Type" or "PublicPort->PrivatePort/Type" + if (parts.length === 2) { publicPart = parts[0]; privatePart = parts[1]; - } else if (parts.length === 1) { // Format like "PrivatePort/Type" + } else if (parts.length === 1) { privatePart = parts[0]; } else { console.warn(`[WebSocket] Skipping unparsable port entry: ${entry}`); continue; } - // Parse Private Part (e.g., "80/tcp") const privateMatch = privatePart.match(/^(\d+)\/(tcp|udp|\w+)$/); if (!privateMatch) { console.warn(`[WebSocket] Skipping unparsable private port part: ${privatePart}`); @@ -111,15 +105,15 @@ function parsePortsString(portsString: string | undefined | null): PortInfo[] { let ip: string | undefined = undefined; let publicPort: number | undefined = undefined; - // Parse Public Part (e.g., "0.0.0.0:8080" or ":::8080" or just "8080") + if (publicPart) { - const publicMatch = publicPart.match(/^(?:([\d.:a-fA-F]+):)?(\d+)$/); // Supports IPv4, IPv6, or just port + const publicMatch = publicPart.match(/^(?:([\d.:a-fA-F]+):)?(\d+)$/); if (publicMatch) { - ip = publicMatch[1] || undefined; // IP might be undefined if only port is specified + ip = publicMatch[1] || undefined; publicPort = parseInt(publicMatch[2], 10); } else { console.warn(`[WebSocket] Skipping unparsable public port part: ${publicPart}`); - // Continue processing with only private port info if public part is weird + } } @@ -134,10 +128,10 @@ function parsePortsString(portsString: string | undefined | null): PortInfo[] { } return ports; } -// --- 结束辅助函数 --- + // 存储所有活动客户端的状态 (key: sessionId) -export const clientStates = new Map(); // Export clientStates +export const clientStates = new Map(); // --- 服务实例化 --- // 将 clientStates 传递给需要访问共享状态的服务 @@ -183,11 +177,11 @@ const cleanupClientConnection = (sessionId: string | undefined) => { console.log(`WebSocket: 会话 ${sessionId} 已清理。`); } else { - // console.log(`WebSocket: 清理时未找到会话 ${sessionId} 的状态。`); + } }; -// --- NEW: Reusable function to fetch remote Docker status with stats --- + const fetchRemoteDockerStatus = async (state: ClientState): Promise<{ available: boolean; containers: DockerContainer[] }> => { if (!state || !state.sshClient) { throw new Error('SSH client is not available in the current state.'); @@ -195,9 +189,9 @@ const fetchRemoteDockerStatus = async (state: ClientState): Promise<{ available: let allContainers: DockerContainer[] = []; const statsMap = new Map(); - let isDockerCmdAvailable = false; // Start assuming unavailable until version check passes + let isDockerCmdAvailable = false; - // --- 1. Check Docker Availability with 'docker version' --- + try { const versionCommand = "docker version --format '{{.Server.Version}}'"; console.log(`[fetchRemoteDockerStatus] Executing: ${versionCommand} on session ${state.ws.sessionId}`); @@ -209,43 +203,43 @@ const fetchRemoteDockerStatus = async (state: ClientState): Promise<{ available: stream.on('data', (data: Buffer) => { stdout += data.toString(); }); stream.stderr.on('data', (data: Buffer) => { stderr += data.toString(); }); stream.on('close', (code: number | null) => { - // Resolve even if code is non-zero, check stderr + resolve({ stdout, stderr }); }); stream.on('error', (execErr: Error) => reject(execErr)); }); }); - // Check stderr for common errors indicating Docker is unavailable or inaccessible + if (versionStderr.includes('command not found') || versionStderr.includes('permission denied') || versionStderr.includes('Cannot connect to the Docker daemon')) { console.warn(`[fetchRemoteDockerStatus] Docker version check failed on session ${state.ws.sessionId}. Docker unavailable or inaccessible. Stderr: ${versionStderr.trim()}`); - return { available: false, containers: [] }; // Docker not available + return { available: false, containers: [] }; } else if (versionStderr) { - // Log other stderr outputs as warnings but proceed + console.warn(`[fetchRemoteDockerStatus] Docker version command stderr on session ${state.ws.sessionId}: ${versionStderr.trim()}`); } - // If stdout has content (version number), Docker is likely available + if (versionStdout.trim()) { console.log(`[fetchRemoteDockerStatus] Docker version check successful on session ${state.ws.sessionId}. Version: ${versionStdout.trim()}`); isDockerCmdAvailable = true; } else { - // If stdout is empty but no critical error in stderr, still assume unavailable + console.warn(`[fetchRemoteDockerStatus] Docker version check on session ${state.ws.sessionId} produced no output, assuming Docker unavailable.`); return { available: false, containers: [] }; } } catch (error: any) { console.error(`[fetchRemoteDockerStatus] Error executing docker version for session ${state.ws.sessionId}:`, error); - // Treat any error during version check as Docker being unavailable + return { available: false, containers: [] }; } - // If version check failed, we already returned. If it passed, isDockerCmdAvailable is true. + - // --- 2. Get basic container info --- + try { const psCommand = "docker ps -a --no-trunc --format '{{json .}}'"; console.log(`[fetchRemoteDockerStatus] Executing: ${psCommand} on session ${state.ws.sessionId}`); @@ -257,44 +251,44 @@ const fetchRemoteDockerStatus = async (state: ClientState): Promise<{ available: stream.on('data', (data: Buffer) => { stdout += data.toString(); }); stream.stderr.on('data', (data: Buffer) => { stderr += data.toString(); }); stream.on('close', (code: number | null) => { - // Don't reject on non-zero code here, check stderr + resolve({ stdout, stderr }); }); stream.on('error', (execErr: Error) => reject(execErr)); }); }); - // Although version check should catch most, double-check ps stderr - if (psStderr.includes('command not found') || // Should not happen if version check passed - psStderr.includes('permission denied') || // Could still happen if permissions differ - psStderr.includes('Cannot connect to the Docker daemon')) { // Should not happen + + if (psStderr.includes('command not found') || + psStderr.includes('permission denied') || + psStderr.includes('Cannot connect to the Docker daemon')) { console.warn(`[fetchRemoteDockerStatus] Docker ps command failed unexpectedly after version check on session ${state.ws.sessionId}. Stderr: ${psStderr.trim()}`); - // Report as available=false, as ps failed critically + return { available: false, containers: [] }; } else if (psStderr) { console.warn(`[fetchRemoteDockerStatus] Docker ps command stderr on session ${state.ws.sessionId}: ${psStderr.trim()}`); - // Continue execution but log the warning + } - // If stdout is empty, there are no containers, which is valid + const lines = psStdout.trim() ? psStdout.trim().split('\n') : []; allContainers = lines .map(line => { try { const data = JSON.parse(line); - // Map raw data to DockerContainer interface (lowercase id) + const container: DockerContainer = { - id: data.ID, // Map ID to lowercase id + id: data.ID, Names: typeof data.Names === 'string' ? data.Names.split(',') : (data.Names || []), Image: data.Image || '', ImageID: data.ImageID || '', Command: data.Command || '', - Created: data.CreatedAt || 0, // Check if CreatedAt exists + Created: data.CreatedAt || 0, State: data.State || 'unknown', Status: data.Status || '', Ports: parsePortsString(data.Ports), Labels: data.Labels || {}, - stats: null // Initialize stats as null + stats: null }; return container; } catch (parseError) { @@ -306,19 +300,19 @@ const fetchRemoteDockerStatus = async (state: ClientState): Promise<{ available: } catch (error: any) { console.error(`[fetchRemoteDockerStatus] Error executing docker ps for session ${state.ws.sessionId}:`, error); - // If ps command fails after version check, report as unavailable + return { available: false, containers: [] }; - // Rethrowing might be too aggressive here, better to report unavailability - // throw new Error(`Failed to get remote Docker container list: ${error.message || error}`); + + } - // --- 3. Get stats for running containers (only if ps was successful) --- - // Check if there are any containers before running stats + + const runningContainerIds = allContainers.filter(c => c.State === 'running').map(c => c.id); if (runningContainerIds.length > 0) { try { - // Construct command to get stats only for running containers + const statsCommand = `docker stats ${runningContainerIds.join(' ')} --no-stream --format '{{json .}}'`; console.log(`[fetchRemoteDockerStatus] Executing: ${statsCommand} on session ${state.ws.sessionId}`); const { stdout: statsStdout, stderr: statsStderr } = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { @@ -329,7 +323,7 @@ const fetchRemoteDockerStatus = async (state: ClientState): Promise<{ available: stream.on('data', (data: Buffer) => { stdout += data.toString(); }); stream.stderr.on('data', (data: Buffer) => { stderr += data.toString(); }); stream.on('close', (code: number | null) => { - // Don't reject on non-zero code, check stderr + resolve({ stdout, stderr }); }); stream.on('error', (execErr: Error) => reject(execErr)); @@ -337,7 +331,7 @@ const fetchRemoteDockerStatus = async (state: ClientState): Promise<{ available: }); if (statsStderr) { - // Log stats errors but don't necessarily fail the whole process + console.warn(`[fetchRemoteDockerStatus] Docker stats command stderr on session ${state.ws.sessionId}: ${statsStderr.trim()}`); } @@ -346,7 +340,7 @@ const fetchRemoteDockerStatus = async (state: ClientState): Promise<{ available: try { const statsData = JSON.parse(line) as DockerStats; if (statsData.ID) { - // Use the ID from stats data (usually short ID) as the key + statsMap.set(statsData.ID, statsData); } } catch (parseError) { @@ -354,32 +348,32 @@ const fetchRemoteDockerStatus = async (state: ClientState): Promise<{ available: } }); } catch (error: any) { - // Failure to get stats is not critical, just log and continue + console.warn(`[fetchRemoteDockerStatus] Error executing docker stats for session ${state.ws.sessionId}:`, error); } } else { console.log(`[fetchRemoteDockerStatus] No running containers found on session ${state.ws.sessionId}, skipping docker stats.`); } - // --- 4. Merge stats into containers --- + allContainers.forEach(container => { - const shortId = container.id.substring(0, 12); // docker stats often uses short ID - const stats = statsMap.get(container.id) || statsMap.get(shortId); // Try matching long and short ID + const shortId = container.id.substring(0, 12); + const stats = statsMap.get(container.id) || statsMap.get(shortId); if (stats) { container.stats = stats; } }); - // If we reached here, Docker is considered available (version check passed) + return { available: true, containers: allContainers }; }; -// --- End fetchRemoteDockerStatus function --- -export const initializeWebSocket = async (server: http.Server, sessionParser: RequestHandler): Promise => { // Make async + +export const initializeWebSocket = async (server: http.Server, sessionParser: RequestHandler): Promise => { const wss = new WebSocketServer({ noServer: true }); - const db = await getDbInstance(); // 获取数据库实例 (use await and getDbInstance) - const DOCKER_STATUS_INTERVAL = 2000; // Poll Docker status every 2 seconds + const db = await getDbInstance(); + const DOCKER_STATUS_INTERVAL = 2000; // --- 心跳检测 --- const heartbeatInterval = setInterval(() => { @@ -499,7 +493,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re // --- 添加日志:打印收到的原始数据 --- console.log(`SSH Data (会话: ${newSessionId}, 原始): `, data.toString()); // 添加原始数据日志 (尝试 utf8) console.log(`SSH Data (会话: ${newSessionId}, Hex): `, data.toString('hex')); // 添加 Hex 日志 - // ------------------------------------ + if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ssh:output', payload: data.toString('base64'), encoding: 'base64' })); } @@ -543,10 +537,10 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re console.log(`WebSocket: 会话 ${newSessionId} 正在启动状态监控...`); statusMonitorService.startStatusPolling(newSessionId); - // 8. Start Docker status polling (using setting) + console.log(`WebSocket: 会话 ${newSessionId} 正在启动 Docker 状态轮询...`); - // --- Get interval from settings --- - let dockerPollIntervalMs = 2000; // Default interval + + let dockerPollIntervalMs = 2000; try { const intervalSetting = await settingsService.getSetting('dockerStatusIntervalSeconds'); if (intervalSetting) { @@ -563,32 +557,32 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re } catch (settingError) { console.error(`[Docker Polling] Error fetching interval setting for session ${newSessionId}. Using default ${dockerPollIntervalMs}ms:`, settingError); } - // --- End get interval --- + const dockerIntervalId = setInterval(async () => { - const currentState = clientStates.get(newSessionId); // Re-fetch state + const currentState = clientStates.get(newSessionId); if (!currentState || currentState.ws.readyState !== WebSocket.OPEN) { console.log(`[Docker Polling] Session ${newSessionId} no longer valid or WS closed. Stopping poll.`); clearInterval(dockerIntervalId); return; } try { - // console.log(`[Docker Polling] Fetching status for session ${newSessionId}...`); + const statusPayload = await fetchRemoteDockerStatus(currentState); if (currentState.ws.readyState === WebSocket.OPEN) { currentState.ws.send(JSON.stringify({ type: 'docker:status:update', payload: statusPayload })); } } catch (error: any) { console.error(`[Docker Polling] Error fetching Docker status for session ${newSessionId}:`, error); - // Optionally send error to client, or just log - // if (currentState.ws.readyState === WebSocket.OPEN) { - // currentState.ws.send(JSON.stringify({ type: 'docker:status:error', payload: { message: `Polling failed: ${error.message}` } })); - // } + + + + } - }, dockerPollIntervalMs); // <-- Use the determined interval + }, dockerPollIntervalMs); newState.dockerStatusIntervalId = dockerIntervalId; - // 9. Trigger initial Docker status fetch immediately + (async () => { const currentState = clientStates.get(newSessionId); if (currentState && currentState.ws.readyState === WebSocket.OPEN) { @@ -601,7 +595,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re } catch (error: any) { console.error(`[Docker Initial Fetch] Error fetching Docker status for session ${newSessionId}:`, error); if (currentState.ws.readyState === WebSocket.OPEN) { - // Send specific error type for initial fetch failure + const errorMessage = error.message || 'Unknown error during initial fetch'; const isUnavailable = errorMessage.includes('command not found') || errorMessage.includes('Cannot connect to the Docker daemon'); if (isUnavailable) { @@ -654,7 +648,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re ws.send(JSON.stringify({ type: 'ssh:error', payload: `连接失败: ${connectError.message}` })); } break; - } // end case 'ssh:connect' + } // --- SSH 输入 --- case 'ssh:input': { @@ -683,7 +677,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re break; } - // --- REFACTORED: Handle Docker Status Request --- + case 'docker:get_status': { if (!state) { console.warn(`WebSocket: 收到来自 ${ws.username} (会话: ${sessionId}) 的 ${type} 请求,但无活动会话状态。`); @@ -697,13 +691,13 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re } console.log(`WebSocket: 处理来自 ${ws.username} (会话: ${sessionId}) 的 ${type} 请求 (手动触发)...`); try { - // Call the reusable function + const statusPayload = await fetchRemoteDockerStatus(state); ws.send(JSON.stringify({ type: 'docker:status:update', payload: statusPayload })); } catch (error: any) { console.error(`WebSocket: 手动执行远程 Docker 状态命令失败 for session ${sessionId}:`, error); const errorMessage = error.message || 'Unknown error fetching status'; - // Send specific error if Docker unavailable, general error otherwise + const isUnavailable = errorMessage.includes('command not found') || errorMessage.includes('Cannot connect to the Docker daemon'); if (isUnavailable) { ws.send(JSON.stringify({ type: 'docker:status:update', payload: { available: false, containers: [] } })); @@ -712,9 +706,9 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re } } break; - } // end case 'docker:get_status' (Refactored) + } - // --- NEW: Handle Docker Command Execution --- + case 'docker:command': { if (!state || !state.sshClient) { console.warn(`WebSocket: 收到来自 ${ws.username} (会话: ${sessionId}) 的 ${type} 请求,但无活动 SSH 连接。`); @@ -743,7 +737,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re console.log(`WebSocket: Validation PASSED for docker:command.`); // 增加成功日志 console.log(`WebSocket: Processing command '${command}' for container '${containerId}' on session ${sessionId}...`); try { - // Sanitize containerId (basic) - more robust validation might be needed + const cleanContainerId = containerId.replace(/[^a-zA-Z0-9_-]/g, ''); if (!cleanContainerId) throw new Error('Invalid container ID format after sanitization.'); @@ -752,11 +746,11 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re case 'start': dockerCliCommand = `docker start ${cleanContainerId}`; break; case 'stop': dockerCliCommand = `docker stop ${cleanContainerId}`; break; case 'restart': dockerCliCommand = `docker restart ${cleanContainerId}`; break; - case 'remove': dockerCliCommand = `docker rm -f ${cleanContainerId}`; break; // Use -f for remove - default: throw new Error(`Unsupported command: ${command}`); // Should be caught by earlier validation + case 'remove': dockerCliCommand = `docker rm -f ${cleanContainerId}`; break; + default: throw new Error(`Unsupported command: ${command}`); } - // Execute command remotely + await new Promise((resolve, reject) => { state.sshClient.exec(dockerCliCommand, { pty: false }, (err, stream) => { if (err) return reject(err); @@ -771,20 +765,20 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re reject(new Error(`Command failed with code ${code}. ${stderr || 'No stderr output.'}`)); } }); - // Add type annotation for execErr + stream.on('error', (execErr: Error) => reject(execErr)); }); }); - // Optionally send a success confirmation back? Not strictly needed if status updates quickly. - // ws.send(JSON.stringify({ type: 'docker:command:success', payload: { command, containerId } })); + + - // Trigger a status update after command execution - // Use a small delay to allow Docker daemon to potentially update state + + setTimeout(() => { - if (clientStates.has(sessionId!)) { // Check if session still exists - ws.send(JSON.stringify({ type: 'request_docker_status_update' })); // Ask frontend to re-request - // Or directly trigger backend fetch and push: - // handleDockerGetStatus(ws, state); // Need to refactor get_status logic into a reusable function + if (clientStates.has(sessionId!)) { + ws.send(JSON.stringify({ type: 'request_docker_status_update' })); + + } }, 500); @@ -794,10 +788,10 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re ws.send(JSON.stringify({ type: 'docker:command:error', payload: { command, containerId, message: `Failed to execute remote command: ${error.message}` } })); } break; - } // end case 'docker:command' + } - // --- SFTP Cases --- + case 'sftp:readdir': case 'sftp:stat': case 'sftp:readfile': @@ -887,7 +881,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re ws.send(JSON.stringify({ type: 'sftp:upload:error', payload: { uploadId: payload?.uploadId, message: '缺少 uploadId, remotePath 或 size' } })); return; } - // TODO: Add audit log for SFTP upload start? + sftpService.startUpload(sessionId, payload.uploadId, payload.remotePath, payload.size); break; } @@ -907,17 +901,17 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re ws.send(JSON.stringify({ type: 'sftp:upload:error', payload: { uploadId: payload?.uploadId, message: '缺少 uploadId' } })); return; } - // TODO: Add audit log for SFTP upload cancel? + sftpService.cancelUpload(sessionId, payload.uploadId); break; } - // --- NEW CASE: Handle docker:get_stats --- + case 'docker:get_stats': { - if (!state || !state.sshClient) { // Check state and sshClient + if (!state || !state.sshClient) { console.warn(`WebSocket: 收到来自 ${ws.username} (会话: ${sessionId}) 的 ${type} 请求,但无活动 SSH 连接。`); ws.send(JSON.stringify({ type: 'docker:stats:error', payload: { containerId: payload?.containerId, message: 'SSH connection not active.' } })); - return; // Use return instead of break inside switch + return; } if (!payload || !payload.containerId) { console.warn(`WebSocket: Invalid payload for docker:get_stats in session ${sessionId}:`, payload); @@ -930,7 +924,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re const command = `docker stats ${containerId} --no-stream --format '{{json .}}'`; try { - // --- FIX: Use sshClient.exec directly --- + const execResult = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { let stdout = ''; let stderr = ''; @@ -939,33 +933,33 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re stream.on('data', (data: Buffer) => { stdout += data.toString(); }); stream.stderr.on('data', (data: Buffer) => { stderr += data.toString(); }); stream.on('close', (code: number | null) => { - // Don't reject on non-zero exit code here, stderr check is more reliable for docker stats + resolve({ stdout, stderr }); }); stream.on('error', (execErr: Error) => reject(execErr)); }); }); - // --- End FIX --- + if (execResult.stderr) { - // Handle cases like container not found or docker errors + console.error(`WebSocket: Docker stats stderr for ${containerId} in session ${sessionId}: ${execResult.stderr}`); ws.send(JSON.stringify({ type: 'docker:stats:error', payload: { containerId, message: execResult.stderr.trim() || 'Error executing stats command.' } })); - return; // Use return after sending error + return; } if (!execResult.stdout) { console.warn(`WebSocket: No stats output for container ${containerId} in session ${sessionId}. Might be stopped or error occurred.`); - // Check stderr again just in case, although previous check should catch most errors + if (!execResult.stderr) { ws.send(JSON.stringify({ type: 'docker:stats:error', payload: { containerId, message: 'No stats data received (container might be stopped).' } })); } - return; // Use return after sending error or warning + return; } try { const statsData = JSON.parse(execResult.stdout.trim()); - // Optional: Clean up or format statsData if needed before sending + ws.send(JSON.stringify({ type: 'docker:stats:update', payload: { containerId, stats: statsData } })); } catch (parseError) { console.error(`WebSocket: Failed to parse docker stats JSON for ${containerId} in session ${sessionId}: ${execResult.stdout}`, parseError); @@ -976,8 +970,8 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re console.error(`WebSocket: Failed to execute docker stats for ${containerId} in session ${sessionId}:`, error); ws.send(JSON.stringify({ type: 'docker:stats:error', payload: { containerId, message: error.message || 'Failed to fetch Docker stats.' } })); } - break; // Break after handling the case - } // --- END CASE: docker:get_stats --- + break; + } default: @@ -1016,4 +1010,4 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re return wss; }; -// --- 移除旧的辅助函数 --- + diff --git a/packages/backend/uploads/backgrounds/1744891110818-458210923-photo_2025_02_21_18_19_37.jpg b/packages/backend/uploads/backgrounds/1744891110818-458210923-photo_2025_02_21_18_19_37.jpg deleted file mode 100644 index d5e1b56..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891110818-458210923-photo_2025_02_21_18_19_37.jpg and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891131360-900833841-electerm_theme_termius_bg.png b/packages/backend/uploads/backgrounds/1744891131360-900833841-electerm_theme_termius_bg.png deleted file mode 100644 index 3d8da0e..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891131360-900833841-electerm_theme_termius_bg.png and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891372507-129203109-electerm_theme_termius_bg.png b/packages/backend/uploads/backgrounds/1744891372507-129203109-electerm_theme_termius_bg.png deleted file mode 100644 index 3d8da0e..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891372507-129203109-electerm_theme_termius_bg.png and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891516681-853695262-electerm_theme_termius_bg.png b/packages/backend/uploads/backgrounds/1744891516681-853695262-electerm_theme_termius_bg.png deleted file mode 100644 index 3d8da0e..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891516681-853695262-electerm_theme_termius_bg.png and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891586352-246836344-electerm_theme_termius_bg.png b/packages/backend/uploads/backgrounds/1744891586352-246836344-electerm_theme_termius_bg.png deleted file mode 100644 index 3d8da0e..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891586352-246836344-electerm_theme_termius_bg.png and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891636121-95985054-photo_2025_02_21_18_19_37.jpg b/packages/backend/uploads/backgrounds/1744891636121-95985054-photo_2025_02_21_18_19_37.jpg deleted file mode 100644 index d5e1b56..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891636121-95985054-photo_2025_02_21_18_19_37.jpg and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891704175-821520443-photo_2025_02_21_18_19_37.jpg b/packages/backend/uploads/backgrounds/1744891704175-821520443-photo_2025_02_21_18_19_37.jpg deleted file mode 100644 index d5e1b56..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891704175-821520443-photo_2025_02_21_18_19_37.jpg and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891708952-69455130-photo_2024_07_23_16_27_29.jpg b/packages/backend/uploads/backgrounds/1744891708952-69455130-photo_2024_07_23_16_27_29.jpg deleted file mode 100644 index 041a952..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891708952-69455130-photo_2024_07_23_16_27_29.jpg and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891768744-518616352-electerm_theme_termius_bg.png b/packages/backend/uploads/backgrounds/1744891768744-518616352-electerm_theme_termius_bg.png deleted file mode 100644 index 3d8da0e..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891768744-518616352-electerm_theme_termius_bg.png and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891781051-54616880-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744891781051-54616880-20200806_225831.jpg deleted file mode 100644 index b355043..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891781051-54616880-20200806_225831.jpg and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891865390-270625090-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744891865390-270625090-20200806_225831.jpg deleted file mode 100644 index b355043..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891865390-270625090-20200806_225831.jpg and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744891972263-47439011-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744891972263-47439011-20200806_225831.jpg deleted file mode 100644 index b355043..0000000 Binary files a/packages/backend/uploads/backgrounds/1744891972263-47439011-20200806_225831.jpg and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744892074360-955625026-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744892074360-955625026-20200806_225831.jpg deleted file mode 100644 index b355043..0000000 Binary files a/packages/backend/uploads/backgrounds/1744892074360-955625026-20200806_225831.jpg and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744892189492-579083255-photo_2025_02_21_18_19_37.jpg b/packages/backend/uploads/backgrounds/1744892189492-579083255-photo_2025_02_21_18_19_37.jpg deleted file mode 100644 index d5e1b56..0000000 Binary files a/packages/backend/uploads/backgrounds/1744892189492-579083255-photo_2025_02_21_18_19_37.jpg and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744892411731-109400587-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744892411731-109400587-20200806_225831.jpg deleted file mode 100644 index b355043..0000000 Binary files a/packages/backend/uploads/backgrounds/1744892411731-109400587-20200806_225831.jpg and /dev/null differ diff --git a/packages/backend/uploads/backgrounds/1744892761635-658966873-20200806_225831.jpg b/packages/backend/uploads/backgrounds/1744892761635-658966873-20200806_225831.jpg deleted file mode 100644 index b355043..0000000 Binary files a/packages/backend/uploads/backgrounds/1744892761635-658966873-20200806_225831.jpg and /dev/null differ