This commit is contained in:
Baobhan Sith
2025-04-20 15:23:58 +08:00
parent 1160f8a514
commit 77cd9272ba
31 changed files with 2781 additions and 2113 deletions
@@ -33,8 +33,9 @@ export const getAllCommandHistory = async (): Promise<CommandHistoryEntry[]> =>
* @returns 返回是否成功删除 (删除行数 > 0)
*/
export const deleteCommandHistoryById = async (id: number): Promise<boolean> => {
const changes = await CommandHistoryRepository.deleteCommandById(id);
return changes > 0;
// deleteCommandById now directly returns boolean indicating success
const success = await CommandHistoryRepository.deleteCommandById(id);
return success;
};
/**
@@ -1,10 +1,13 @@
// packages/backend/src/services/import-export.service.ts
import * as ConnectionRepository from '../repositories/connection.repository';
import * as ProxyRepository from '../repositories/proxy.repository';
import { getDb } from '../database'; // Need db instance for transaction
// Import the instance getter and helpers
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { Database } from 'sqlite3'; // Import Database type
const db = getDb(); // Get db instance for transaction management
// Remove top-level db instance
// Define structure for imported connection data (can be shared in types)
// --- Interface definitions remain the same ---
interface ImportedConnectionData {
name: string;
host: string;
@@ -27,35 +30,40 @@ interface ImportedConnectionData {
encrypted_passphrase?: string | null;
} | null;
}
// Define structure for exported connection data (can be shared in types)
interface ExportedConnectionData extends Omit<ImportedConnectionData, 'id'> {
// Exclude fields not needed for export like id, created_at etc.
}
// Define structure for import results
interface ExportedConnectionData extends Omit<ImportedConnectionData, 'id'> {}
export interface ImportResult {
successCount: number;
failureCount: number;
errors: { connectionName?: string; message: string }[];
}
// --- End Interface definitions ---
/**
* 导出所有连接配置
*/
export const exportConnections = async (): Promise<ExportedConnectionData[]> => {
// 1. Fetch all connections with tags (basic info)
// We need full connection info including encrypted fields and proxy details for export
// Let's adapt the repository or add a new method if needed.
// For now, let's assume findFullConnectionById can be adapted or a similar findAll method exists.
// Re-using the logic from controller for now, ideally repo handles joins.
try {
const db = await getDbInstance(); // Get DB instance
const connectionsWithProxies = await new Promise<any[]>((resolve, reject) => {
db.all(
// Define a more specific type for the row structure
type ExportRow = ConnectionRepository.FullConnectionData & {
proxy_db_id: number | null;
proxy_name: string | null;
proxy_type: 'SOCKS5' | 'HTTP' | null;
proxy_host: string | null;
proxy_port: number | null;
proxy_username: string | null;
proxy_auth_method: 'none' | 'password' | 'key' | null;
proxy_encrypted_password?: string | null;
proxy_encrypted_private_key?: string | null;
proxy_encrypted_passphrase?: string | null;
};
// Fetch connections joined with proxies using await allDb
const connectionsWithProxies = await allDb<ExportRow>(db,
`SELECT
c.id, c.name, c.host, c.port, c.username, c.auth_method,
c.encrypted_password, c.encrypted_private_key, c.encrypted_passphrase,
c.proxy_id,
c.*,
p.id as proxy_db_id, p.name as proxy_name, p.type as proxy_type,
p.host as proxy_host, p.port as proxy_port, p.username as proxy_username,
p.auth_method as proxy_auth_method,
@@ -64,64 +72,58 @@ export const exportConnections = async (): Promise<ExportedConnectionData[]> =>
p.encrypted_passphrase as proxy_encrypted_passphrase
FROM connections c
LEFT JOIN proxies p ON c.proxy_id = p.id
ORDER BY c.name ASC`,
(err, rows: any[]) => {
if (err) {
console.error('Service: 查询连接和代理信息以供导出时出错:', err.message);
return reject(new Error('导出连接失败:查询连接信息出错'));
}
resolve(rows);
}
ORDER BY c.name ASC`
);
});
const connectionTags = await new Promise<{[connId: number]: number[]}>((resolve, reject) => {
db.all('SELECT connection_id, tag_id FROM connection_tags', (err, rows: {connection_id: number, tag_id: number}[]) => {
if (err) {
console.error('Service: 查询连接标签以供导出时出错:', err.message);
return reject(new Error('导出连接失败:查询标签信息出错'));
}
const tagsMap: {[connId: number]: number[]} = {};
rows.forEach(row => {
if (!tagsMap[row.connection_id]) tagsMap[row.connection_id] = [];
tagsMap[row.connection_id].push(row.tag_id);
});
resolve(tagsMap);
// 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);
});
});
// 2. Format data for export
const formattedData: ExportedConnectionData[] = connectionsWithProxies.map(row => {
const connection: ExportedConnectionData = {
name: row.name,
host: row.host,
port: row.port,
username: row.username,
auth_method: row.auth_method,
encrypted_password: row.encrypted_password,
encrypted_private_key: row.encrypted_private_key,
encrypted_passphrase: row.encrypted_passphrase,
tag_ids: connectionTags[row.id] || [],
proxy: null // Initialize proxy as null
};
if (row.proxy_db_id) {
connection.proxy = {
name: row.proxy_name,
type: row.proxy_type,
host: row.proxy_host,
port: row.proxy_port,
username: row.proxy_username,
auth_method: row.proxy_auth_method,
encrypted_password: row.proxy_encrypted_password,
encrypted_private_key: row.proxy_encrypted_private_key,
encrypted_passphrase: row.proxy_encrypted_passphrase,
// Format data for export
const formattedData: ExportedConnectionData[] = connectionsWithProxies.map(row => {
const connection: ExportedConnectionData = {
name: row.name ?? 'Unnamed', // Provide default if name is null
host: row.host,
port: row.port,
username: row.username,
auth_method: row.auth_method,
encrypted_password: row.encrypted_password,
encrypted_private_key: row.encrypted_private_key,
encrypted_passphrase: row.encrypted_passphrase,
tag_ids: tagsMap[row.id] || [],
proxy: null
};
}
return connection;
});
return formattedData;
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
username: row.proxy_username,
auth_method: row.proxy_auth_method ?? 'none', // Provide default
encrypted_password: row.proxy_encrypted_password,
encrypted_private_key: row.proxy_encrypted_private_key,
encrypted_passphrase: row.proxy_encrypted_passphrase,
};
}
return connection;
});
return formattedData;
} catch (err: any) {
console.error('Service: 导出连接时出错:', err.message);
throw new Error(`导出连接失败: ${err.message}`); // Re-throw for controller
}
};
@@ -139,153 +141,127 @@ export const importConnections = async (fileBuffer: Buffer): Promise<ImportResul
}
} catch (error: any) {
console.error('Service: 解析导入文件失败:', error);
throw new Error(`解析 JSON 文件失败: ${error.message}`); // Re-throw for controller
throw new Error(`解析 JSON 文件失败: ${error.message}`);
}
let successCount = 0;
let failureCount = 0;
const errors: { connectionName?: string; message: string }[] = [];
const connectionsToInsert: Omit<ConnectionRepository.FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'>[] = [];
const db = await getDbInstance(); // Get DB instance once for the transaction
// Use a transaction for atomicity
return new Promise<ImportResult>((resolveOuter, rejectOuter) => {
db.serialize(() => {
db.run('BEGIN TRANSACTION', async (beginErr: Error | null) => {
if (beginErr) {
console.error('Service: 开始导入事务失败:', beginErr);
return rejectOuter(new Error(`开始事务失败: ${beginErr.message}`));
try {
await runDb(db, 'BEGIN TRANSACTION'); // Start transaction using await runDb
const connectionsToInsert: Array<Omit<ConnectionRepository.FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { tag_ids?: number[] }> = [];
const proxyCache: { [key: string]: number } = {}; // Cache for created/found proxy IDs
// --- 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) ...
try {
// Process each connection data from the imported file
for (const connData of importedData) {
try {
// 1. Validate connection data (basic)
if (!connData.name || !connData.host || !connData.port || !connData.username || !connData.auth_method) {
throw new Error('缺少必要的连接字段 (name, host, port, username, auth_method)。');
}
if (connData.auth_method === 'password' && !connData.encrypted_password) {
throw new Error('密码认证缺少 encrypted_password。');
}
if (connData.auth_method === 'key' && !connData.encrypted_private_key) {
throw new Error('密钥认证缺少 encrypted_private_key。');
}
// Add more validation as needed
let proxyIdToUse: number | null = null;
let proxyIdToUse: number | null = null;
// 2. Handle proxy (find or create)
if (connData.proxy) {
const proxyData = connData.proxy;
// Validate proxy data
if (!proxyData.name || !proxyData.type || !proxyData.host || !proxyData.port) {
throw new Error('代理信息不完整 (缺少 name, type, host, port)。');
}
// Add more proxy validation if needed
// Try to find existing proxy
const existingProxy = await ProxyRepository.findProxyByNameTypeHostPort(proxyData.name, proxyData.type, proxyData.host, proxyData.port);
if (existingProxy) {
proxyIdToUse = existingProxy.id;
} else {
// Proxy doesn't exist, create it
const newProxyData = {
name: proxyData.name,
type: proxyData.type,
host: proxyData.host,
port: proxyData.port,
username: proxyData.username || null,
auth_method: proxyData.auth_method || 'none',
encrypted_password: proxyData.encrypted_password || null,
encrypted_private_key: proxyData.encrypted_private_key || null,
encrypted_passphrase: proxyData.encrypted_passphrase || null,
};
proxyIdToUse = await ProxyRepository.createProxy(newProxyData);
console.log(`Service: 导入连接 ${connData.name}: 新代理 ${proxyData.name} 创建成功 (ID: ${proxyIdToUse})`);
}
}
// 3. Prepare connection data for bulk insert
connectionsToInsert.push({
name: connData.name,
host: connData.host,
port: connData.port,
username: connData.username,
auth_method: connData.auth_method,
encrypted_password: connData.encrypted_password || null,
encrypted_private_key: connData.encrypted_private_key || null,
encrypted_passphrase: connData.encrypted_passphrase || null,
proxy_id: proxyIdToUse,
// tag_ids will be handled separately after insertion
});
} catch (connError: any) {
// Error processing this specific connection
failureCount++;
errors.push({ connectionName: connData.name || '未知连接', message: connError.message });
console.warn(`Service: 处理导入连接 "${connData.name || '未知'}" 时出错: ${connError.message}`);
}
} // End for loop
// 4. Bulk insert connections
let insertedResults: { connectionId: number, originalData: any }[] = [];
if (connectionsToInsert.length > 0) {
insertedResults = await ConnectionRepository.bulkInsertConnections(connectionsToInsert);
successCount = insertedResults.length;
// 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) {
throw new Error('代理信息不完整 (缺少 name, type, host, port)。');
}
// 5. Associate tags for successfully inserted connections
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) {
try {
await ConnectionRepository.updateConnectionTags(result.connectionId, validTagIds);
} catch (tagError: any) {
// Log warning but don't fail the entire import for tag association error
console.warn(`Service: 导入连接 ${result.originalData.name}: 关联标签失败 (ID: ${result.connectionId}): ${tagError.message}`);
// Optionally, add this to the 'errors' array reported back
errors.push({ connectionName: result.originalData.name, message: `关联标签失败: ${tagError.message}` });
// Decrement successCount or increment failureCount if tag failure should count as overall failure
// failureCount++; // Example: Count tag failures
}
}
}
}
// 6. Commit or Rollback
if (failureCount > 0 && successCount === 0) { // Only rollback if ALL fail, or adjust logic as needed
console.warn(`Service: 导入连接存在 ${failureCount} 个错误,且无成功记录,正在回滚事务...`);
db.run('ROLLBACK', (rollbackErr: Error | null) => {
if (rollbackErr) console.error("Service: 回滚事务失败:", rollbackErr);
// Reject outer promise with collected errors
rejectOuter(new Error(`导入失败,存在 ${failureCount} 个错误。`));
});
const cacheKey = `${proxyData.name}-${proxyData.type}-${proxyData.host}-${proxyData.port}`;
if (proxyCache[cacheKey]) {
proxyIdToUse = proxyCache[cacheKey];
} else {
// Commit even if some failed, report partial success
db.run('COMMIT', (commitErr: Error | null) => {
if (commitErr) {
console.error('Service: 提交导入事务时出错:', commitErr);
rejectOuter(new Error(`提交导入事务失败: ${commitErr.message}`));
} else {
console.log(`Service: 导入事务提交。成功: ${successCount}, 失败: ${failureCount}`);
resolveOuter({ successCount, failureCount, errors }); // Resolve outer promise
}
});
const existingProxy = await ProxyRepository.findProxyByNameTypeHostPort(proxyData.name, proxyData.type, proxyData.host, proxyData.port);
if (existingProxy) {
proxyIdToUse = existingProxy.id;
} else {
const newProxyData: Omit<ProxyRepository.ProxyData, 'id' | 'created_at' | 'updated_at'> = {
name: proxyData.name,
type: proxyData.type,
host: proxyData.host,
port: proxyData.port,
username: proxyData.username || null,
auth_method: proxyData.auth_method || 'none',
encrypted_password: proxyData.encrypted_password || null,
encrypted_private_key: proxyData.encrypted_private_key || null,
encrypted_passphrase: proxyData.encrypted_passphrase || null,
};
proxyIdToUse = await ProxyRepository.createProxy(newProxyData);
console.log(`Service: 导入连接 ${connData.name}: 新代理 ${proxyData.name} 创建成功 (ID: ${proxyIdToUse})`);
}
if (proxyIdToUse) proxyCache[cacheKey] = proxyIdToUse; // Cache the ID
}
} catch (innerError: any) {
// Catch errors during the process (e.g., bulk insert failure)
console.error('Service: 导入事务内部出错:', innerError);
db.run('ROLLBACK', (rollbackErr: Error | null) => {
if (rollbackErr) console.error("Service: 回滚事务失败:", rollbackErr);
rejectOuter(innerError); // Reject outer promise
});
}
}); // End BEGIN TRANSACTION
}); // End db.serialize
}); // End new Promise
// Prepare connection data for bulk insert (add tag_ids here)
connectionsToInsert.push({
name: connData.name,
host: connData.host,
port: connData.port,
username: connData.username,
auth_method: connData.auth_method,
encrypted_password: connData.encrypted_password || null,
encrypted_private_key: connData.encrypted_private_key || null,
encrypted_passphrase: connData.encrypted_passphrase || null,
proxy_id: proxyIdToUse,
tag_ids: connData.tag_ids || [] // Include tag_ids
});
} catch (connError: any) {
failureCount++;
errors.push({ connectionName: connData.name || '未知连接', message: connError.message });
console.warn(`Service: 处理导入连接 "${connData.name || '未知'}" 时出错: ${connError.message}`);
}
} // End for loop
// --- Pass 2: Bulk insert connections ---
let insertedResults: { connectionId: number, originalData: any }[] = [];
if (connectionsToInsert.length > 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
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
console.warn(`Service: 导入连接 ${result.originalData.name}: 关联标签 ID ${tagId} 失败: ${tagError.message}`);
})
);
await Promise.all(tagPromises);
}
}
}
// Commit transaction using await runDb
await runDb(db, 'COMMIT');
console.log(`Service: 导入事务提交。成功: ${successCount}, 失败: ${failureCount}`);
return { successCount, failureCount, errors };
} catch (error: any) {
// Rollback transaction on any error during the process
console.error('Service: 导入事务处理出错,正在回滚:', error);
try {
await runDb(db, 'ROLLBACK'); // Use await runDb
} catch (rollbackErr: any) {
console.error("Service: 回滚事务失败:", rollbackErr);
}
// Adjust failure count and return error summary
failureCount = importedData.length;
successCount = 0;
errors.push({ message: `事务处理失败: ${error.message}` });
return { successCount, failureCount, errors };
}
};
@@ -1,9 +1,12 @@
import { getDb } from '../database';
// packages/backend/src/services/ip-blacklist.service.ts
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { settingsService } from './settings.service';
import { NotificationService } from './notification.service'; // 导入 NotificationService
import * as sqlite3 from 'sqlite3';
import * as sqlite3 from 'sqlite3'; // Keep for RunResult type if needed
const db = getDb();
// Remove top-level db instance
// const db = getDb();
const notificationService = new NotificationService(); // 实例化 NotificationService
// 黑名单相关设置的 Key
@@ -25,6 +28,9 @@ interface IpBlacklistEntry {
blocked_until: number | null;
}
// Define the expected row structure from the database if it matches IpBlacklistEntry
type DbIpBlacklistRow = IpBlacklistEntry;
export class IpBlacklistService {
/**
@@ -33,15 +39,14 @@ export class IpBlacklistService {
* @returns 黑名单记录或 undefined
*/
private async getEntry(ip: string): Promise<IpBlacklistEntry | undefined> {
return new Promise((resolve, reject) => {
db.get('SELECT * FROM ip_blacklist WHERE ip = ?', [ip], (err, row: IpBlacklistEntry) => {
if (err) {
console.error(`[IP Blacklist] 查询 IP ${ip} 时出错:`, err.message);
return reject(new Error('数据库查询失败'));
}
resolve(row);
});
});
try {
const db = await getDbInstance();
const row = await getDbRow<DbIpBlacklistRow>(db, 'SELECT * FROM ip_blacklist WHERE ip = ?', [ip]);
return row; // Returns undefined if not found
} catch (err: any) {
console.error(`[IP Blacklist] 查询 IP ${ip} 时出错:`, err.message);
throw new Error('数据库查询失败'); // Re-throw error
}
}
/**
@@ -60,11 +65,10 @@ export class IpBlacklistService {
console.log(`[IP Blacklist] IP ${ip} 当前被封禁,直到 ${new Date(entry.blocked_until * 1000).toISOString()}`);
return true; // 仍在封禁期内
}
// 如果封禁时间已过或为 null,则不再封禁
return false;
} catch (error) {
console.error(`[IP Blacklist] 检查 IP ${ip} 封禁状态时出错:`, error);
return false; // 出错时默认不封禁,避免锁死用户
} catch (error: any) { // Catch errors from getEntry
console.error(`[IP Blacklist] 检查 IP ${ip} 封禁状态时出错:`, error.message);
return false; // 出错时默认不封禁
}
}
@@ -74,7 +78,6 @@ export class IpBlacklistService {
* @param ip IP 地址
*/
async recordFailedAttempt(ip: string): Promise<void> {
// 如果是本地 IP,则不记录失败尝试,直接返回
if (LOCAL_IPS.includes(ip)) {
console.log(`[IP Blacklist] 检测到本地 IP ${ip} 登录失败,跳过黑名单处理。`);
return;
@@ -82,83 +85,75 @@ export class IpBlacklistService {
const now = Math.floor(Date.now() / 1000);
try {
// 获取设置,并提供默认值处理
const db = await getDbInstance();
const maxAttemptsStr = await settingsService.getSetting(MAX_LOGIN_ATTEMPTS_KEY);
const banDurationStr = await settingsService.getSetting(LOGIN_BAN_DURATION_KEY);
// 解析设置值,如果无效或未设置,则使用默认值
const maxAttempts = parseInt(maxAttemptsStr || '5', 10) || 5;
const banDuration = parseInt(banDurationStr || '300', 10) || 300;
const entry = await this.getEntry(ip);
if (entry) {
// 更新现有记录
// Update existing record
const newAttempts = entry.attempts + 1;
let blockedUntil = entry.blocked_until;
let shouldNotify = false;
// 检查是否达到封禁阈值
if (newAttempts >= maxAttempts && !entry.blocked_until) { // 只有在之前未被封禁时才触发通知
if (newAttempts >= maxAttempts && !entry.blocked_until) { // Only block and notify if not already blocked
blockedUntil = now + banDuration;
shouldNotify = true;
console.warn(`[IP Blacklist] IP ${ip} 登录失败次数达到 ${newAttempts} 次 (阈值 ${maxAttempts}),将被封禁 ${banDuration} 秒。`);
// 触发 IP_BLACKLISTED 通知
} else if (newAttempts >= maxAttempts && entry.blocked_until) {
console.log(`[IP Blacklist] IP ${ip} 再次登录失败,当前已处于封禁状态。`);
// Optionally extend ban duration here if needed
}
await runDb(db,
'UPDATE ip_blacklist SET attempts = ?, last_attempt_at = ?, blocked_until = ? WHERE ip = ?',
[newAttempts, now, blockedUntil, ip]
);
if (shouldNotify && blockedUntil) {
// Trigger notification after successful DB update
notificationService.sendNotification('IP_BLACKLISTED', {
ip: ip,
attempts: newAttempts,
duration: banDuration, // 封禁时长(秒)
blockedUntil: new Date(blockedUntil * 1000).toISOString() // 封禁截止时间
duration: banDuration,
blockedUntil: new Date(blockedUntil * 1000).toISOString()
}).catch(err => console.error(`[IP Blacklist] 发送 IP_BLACKLISTED 通知失败 for IP ${ip}:`, err));
} else if (newAttempts >= maxAttempts && entry.blocked_until) {
// 如果已经达到阈值且已被封禁,可能需要更新封禁时间(如果策略是每次失败都延长)
// 当前逻辑是只在首次达到阈值时设置封禁时间,后续失败只增加次数
console.log(`[IP Blacklist] IP ${ip} 再次登录失败,当前已处于封禁状态。`);
}
await new Promise<void>((resolve, reject) => {
db.run(
'UPDATE ip_blacklist SET attempts = ?, last_attempt_at = ?, blocked_until = ? WHERE ip = ?',
[newAttempts, now, blockedUntil, ip],
(err) => {
if (err) {
console.error(`[IP Blacklist] 更新 IP ${ip} 失败尝试次数时出错:`, err.message);
return reject(err);
}
resolve();
}
);
});
} else {
// 插入新记录
// Insert new record
let blockedUntil: number | null = null;
const attempts = 1; // 首次尝试
if (attempts >= maxAttempts) { // 首次尝试就达到阈值
const attempts = 1;
let shouldNotify = false;
if (attempts >= maxAttempts) {
blockedUntil = now + banDuration;
console.warn(`[IP Blacklist] IP ${ip} 首次登录失败即达到阈值 ${maxAttempts},将被封禁 ${banDuration} 秒。`);
// 触发 IP_BLACKLISTED 通知
shouldNotify = true;
console.warn(`[IP Blacklist] IP ${ip} 首次登录失败即达到阈值 ${maxAttempts},将被封禁 ${banDuration} 秒。`);
}
await runDb(db,
'INSERT INTO ip_blacklist (ip, attempts, last_attempt_at, blocked_until) VALUES (?, ?, ?, ?)',
[ip, attempts, now, blockedUntil]
);
if (shouldNotify && blockedUntil) {
// Trigger notification after successful DB insert
notificationService.sendNotification('IP_BLACKLISTED', {
ip: ip,
attempts: attempts,
duration: banDuration,
blockedUntil: new Date(blockedUntil * 1000).toISOString()
}).catch(err => console.error(`[IP Blacklist] 发送 IP_BLACKLISTED 通知失败 for IP ${ip}:`, err));
}
await new Promise<void>((resolve, reject) => {
db.run(
'INSERT INTO ip_blacklist (ip, attempts, last_attempt_at, blocked_until) VALUES (?, 1, ?, ?)',
[ip, now, blockedUntil],
(err) => {
if (err) {
console.error(`[IP Blacklist] 插入新 IP ${ip} 失败记录时出错:`, err.message);
return reject(err);
}
resolve();
}
);
});
}
}
} catch (error) {
console.error(`[IP Blacklist] 记录 IP ${ip} 失败尝试时出错:`, error);
} 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,19 +163,12 @@ export class IpBlacklistService {
*/
async resetAttempts(ip: string): Promise<void> {
try {
await new Promise<void>((resolve, reject) => {
// 直接删除记录,或者将 attempts 重置为 0 并清除 blocked_until
db.run('DELETE FROM ip_blacklist WHERE ip = ?', [ip], (err) => {
if (err) {
console.error(`[IP Blacklist] 重置 IP ${ip} 尝试次数时出错:`, err.message);
return reject(err);
}
console.log(`[IP Blacklist] 已重置 IP ${ip} 的失败尝试记录。`);
resolve();
});
});
} catch (error) {
console.error(`[IP Blacklist] 重置 IP ${ip} 尝试次数时出错:`, error);
const db = await getDbInstance();
await runDb(db, 'DELETE FROM ip_blacklist WHERE ip = ?', [ip]);
console.log(`[IP Blacklist] 已重置 IP ${ip} 的失败尝试记录。`);
} catch (error: any) {
console.error(`[IP Blacklist] 重置 IP ${ip} 尝试次数时出错:`, error.message);
// Avoid throwing error here
}
}
@@ -190,53 +178,42 @@ export class IpBlacklistService {
* @param offset 偏移量
*/
async getBlacklist(limit: number = 50, offset: number = 0): Promise<{ entries: IpBlacklistEntry[], total: number }> {
const entries = await new Promise<IpBlacklistEntry[]>((resolve, reject) => {
db.all('SELECT * FROM ip_blacklist ORDER BY last_attempt_at DESC LIMIT ? OFFSET ?', [limit, offset], (err, rows: IpBlacklistEntry[]) => {
if (err) {
console.error('[IP Blacklist] 获取黑名单列表时出错:', err.message);
return reject(new Error('数据库查询失败'));
}
resolve(rows);
});
});
const total = await new Promise<number>((resolve, reject) => {
db.get('SELECT COUNT(*) as count FROM ip_blacklist', (err, row: { count: number }) => {
if (err) {
console.error('[IP Blacklist] 获取黑名单总数时出错:', err.message);
return reject(0); // 出错时返回 0
}
resolve(row.count);
});
});
return { entries, total };
try {
const db = await getDbInstance();
const entries = await allDb<DbIpBlacklistRow>(db,
'SELECT * FROM ip_blacklist ORDER BY last_attempt_at DESC LIMIT ? OFFSET ?',
[limit, offset]
);
const countRow = await getDbRow<{ count: number }>(db, 'SELECT COUNT(*) as count FROM ip_blacklist');
const total = countRow?.count ?? 0;
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
}
}
/**
* 从黑名单中删除一个 IP (解除封禁)
* @param ip IP 地址
* @returns Promise<boolean> 是否成功删除
*/
async removeFromBlacklist(ip: string): Promise<void> {
async removeFromBlacklist(ip: string): Promise<boolean> {
try {
await new Promise<void>((resolve, reject) => {
// 将 this 类型改回 RunResult 以访问 changes 属性
db.run('DELETE FROM ip_blacklist WHERE ip = ?', [ip], function(this: sqlite3.RunResult, err: Error | null) {
if (err) {
console.error(`[IP Blacklist] 从黑名单删除 IP ${ip} 时出错:`, err.message);
return reject(err);
}
if (this.changes === 0) {
console.warn(`[IP Blacklist] 尝试删除 IP ${ip},但该 IP 不在黑名单中。`);
} else {
console.log(`[IP Blacklist] 从黑名单删除 IP ${ip}`);
}
resolve();
});
});
} catch (error) {
console.error(`[IP Blacklist] 从黑名单删除 IP ${ip} 时出错:`, error);
throw error; // 重新抛出错误,以便上层处理
const db = await getDbInstance();
const result = await runDb(db, 'DELETE FROM ip_blacklist WHERE ip = ?', [ip]);
if (result.changes > 0) {
console.log(`[IP Blacklist] 已从黑名单中删除 IP ${ip}`);
return true;
} else {
console.warn(`[IP Blacklist] 尝试删除 IP ${ip},但该 IP 不在黑名单中。`);
return false;
}
} catch (error: any) {
console.error(`[IP Blacklist] 从黑名单删除 IP ${ip} 时出错:`, error.message);
throw new Error(`从黑名单删除 IP ${ip} 时出错`); // Re-throw error
}
}
}
@@ -32,7 +32,7 @@ export const updateQuickCommand = async (id: number, name: string | null, comman
}
const finalName = name && name.trim().length > 0 ? name.trim() : null;
const changes = await QuickCommandsRepository.updateQuickCommand(id, finalName, command.trim());
return changes > 0;
return changes;
};
/**
@@ -42,7 +42,7 @@ export const updateQuickCommand = async (id: number, name: string | null, comman
*/
export const deleteQuickCommand = async (id: number): Promise<boolean> => {
const changes = await QuickCommandsRepository.deleteQuickCommand(id);
return changes > 0;
return changes;
};
/**
@@ -61,7 +61,7 @@ export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'name'):
*/
export const incrementUsageCount = async (id: number): Promise<boolean> => {
const changes = await QuickCommandsRepository.incrementUsageCount(id);
return changes > 0;
return changes;
};
/**
+24 -12
View File
@@ -49,11 +49,12 @@ export const getConnectionDetails = async (connectionId: number): Promise<Decryp
try {
const fullConnInfo: DecryptedConnectionDetails = {
id: rawConnInfo.id,
name: rawConnInfo.name,
host: rawConnInfo.host,
port: rawConnInfo.port,
username: rawConnInfo.username,
auth_method: rawConnInfo.auth_method,
// Add null check for required fields from rawConnInfo
name: rawConnInfo.name ?? (() => { throw new Error(`Connection ID ${connectionId} has null name.`); })(),
host: rawConnInfo.host ?? (() => { throw new Error(`Connection ID ${connectionId} has null host.`); })(),
port: rawConnInfo.port ?? (() => { throw new Error(`Connection ID ${connectionId} has null port.`); })(),
username: rawConnInfo.username ?? (() => { throw new Error(`Connection ID ${connectionId} has null username.`); })(),
auth_method: rawConnInfo.auth_method ?? (() => { throw new Error(`Connection ID ${connectionId} has null auth_method.`); })(),
password: (rawConnInfo.auth_method === 'password' && rawConnInfo.encrypted_password) ? decrypt(rawConnInfo.encrypted_password) : undefined,
privateKey: (rawConnInfo.auth_method === 'key' && rawConnInfo.encrypted_private_key) ? decrypt(rawConnInfo.encrypted_private_key) : undefined,
passphrase: (rawConnInfo.auth_method === 'key' && rawConnInfo.encrypted_passphrase) ? decrypt(rawConnInfo.encrypted_passphrase) : undefined,
@@ -61,14 +62,25 @@ export const getConnectionDetails = async (connectionId: number): Promise<Decryp
};
if (rawConnInfo.proxy_db_id) {
// Add null checks for required proxy fields inside the if block
const proxyName = rawConnInfo.proxy_name ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null name.`); })();
const proxyType = rawConnInfo.proxy_type ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null type.`); })();
const proxyHost = rawConnInfo.proxy_host ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null host.`); })();
const proxyPort = rawConnInfo.proxy_port ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null port.`); })();
// Ensure proxyType is one of the allowed values
if (proxyType !== 'SOCKS5' && proxyType !== 'HTTP') {
throw new Error(`Proxy for Connection ID ${connectionId} has invalid type: ${proxyType}`);
}
fullConnInfo.proxy = {
id: rawConnInfo.proxy_db_id,
name: rawConnInfo.proxy_name,
type: rawConnInfo.proxy_type,
host: rawConnInfo.proxy_host,
port: rawConnInfo.proxy_port,
username: rawConnInfo.proxy_username || undefined,
password: rawConnInfo.proxy_encrypted_password ? decrypt(rawConnInfo.proxy_encrypted_password) : undefined,
id: rawConnInfo.proxy_db_id, // Already checked by the if condition
name: proxyName,
type: proxyType, // Already validated
host: proxyHost,
port: proxyPort,
username: rawConnInfo.proxy_username || undefined, // Optional, defaults to undefined
password: rawConnInfo.proxy_encrypted_password ? decrypt(rawConnInfo.proxy_encrypted_password) : undefined, // Optional, handled by decrypt logic
// 可以根据需要解密代理的其他凭证
};
}