Files
nexus-terminal/packages/backend/src/connections/connection.service.ts
T
yinjianm 1081c73254 feat(connection): 支持已保存登录凭证并重构首页仪表盘
新增登录凭证管理接口、数据表与前端选择器,连接创建、
编辑和测试现已支持复用已保存凭证

重构首页为管理驾驶舱,增加统计卡片、趋势与分布图、
活跃连接排行,并通过 summary 聚合数据统一驱动
2026-03-26 00:17:35 +08:00

627 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as ConnectionRepository from './connection.repository';
import { encrypt, decrypt } from '../utils/crypto';
import { AuditLogService } from '../audit/audit.service';
import * as SshKeyService from '../ssh_keys/ssh_key.service';
import * as LoginCredentialService from '../login-credentials/login-credential.service';
import {
ConnectionBase,
ConnectionWithTags,
CreateConnectionInput,
UpdateConnectionInput,
FullConnectionData,
ConnectionWithTags as ConnectionWithTagsType // Alias to avoid conflict with variable name
} from '../types/connection.types';
export type { ConnectionBase, ConnectionWithTags, CreateConnectionInput, UpdateConnectionInput };
/**
* 辅助函数:验证 jump_chain 并处理与 proxy_id 的互斥关系
* @param jumpChain 输入的 jump_chain
* @param proxyId 输入的 proxy_id
* @param connectionId 当前正在操作的连接ID (仅在更新时提供)
* @returns 处理过的 jump_chain (null 如果无效或应被忽略)
* @throws Error 如果验证失败
*/
const _validateAndProcessJumpChain = async (
jumpChain: number[] | null | undefined,
proxyId: number | null | undefined,
connectionId?: number
): Promise<number[] | null> => {
if (!jumpChain || jumpChain.length === 0) {
return null;
}
const validatedChain: number[] = [];
for (const id of jumpChain) {
if (typeof id !== 'number') {
throw new Error('jump_chain 中的 ID 必须是数字。');
}
if (connectionId && id === connectionId) {
throw new Error(`jump_chain 不能包含当前连接自身的 ID (${connectionId})。`);
}
const existingConnection = await ConnectionRepository.findConnectionByIdWithTags(id);
if (!existingConnection) {
throw new Error(`jump_chain 中的连接 ID ${id} 未找到。`);
}
if (existingConnection.type !== 'SSH') {
throw new Error(`jump_chain 中的连接 ID ${id} (${existingConnection.name}) 不是 SSH 类型。`);
}
validatedChain.push(id);
}
return validatedChain.length > 0 ? validatedChain : null;
};
const auditLogService = new AuditLogService();
const _getSavedCredentialSnapshot = async (
loginCredentialId: number,
connectionType: 'SSH' | 'RDP' | 'VNC'
) => {
const credential = await LoginCredentialService.getLoginCredentialById(loginCredentialId);
if (!credential) {
throw new Error(`登录凭证 ID ${loginCredentialId} 不存在。`);
}
if (credential.type !== connectionType) {
throw new Error(`登录凭证类型 ${credential.type} 与连接类型 ${connectionType} 不匹配。`);
}
return credential;
};
/**
* 获取所有连接(包含标签)
*/
export const getAllConnections = async (): Promise<ConnectionWithTags[]> => {
// Repository now returns ConnectionWithTags including 'type'
// Explicit type assertion to ensure compatibility
return ConnectionRepository.findAllConnectionsWithTags() as Promise<ConnectionWithTags[]>;
};
/**
* 根据 ID 获取单个连接(包含标签)
*/
export const getConnectionById = async (id: number): Promise<ConnectionWithTags | null> => {
// Repository now returns ConnectionWithTags including 'type'
// Explicit type assertion to ensure compatibility
return ConnectionRepository.findConnectionByIdWithTags(id) as Promise<ConnectionWithTags | null>;
};
/**
* 创建新连接
*/
export const createConnection = async (input: CreateConnectionInput): Promise<ConnectionWithTags> => {
type ConnectionDataForRepo = Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at' | 'tag_ids'> & { jump_chain?: number[] | null; proxy_type?: 'proxy' | 'jump' | null };
console.log('[Service:createConnection] Received input:', JSON.stringify(input, null, 2)); // Log input
const processedJumpChain = await _validateAndProcessJumpChain(input.jump_chain, input.proxy_id);
const connectionType = input.type?.toUpperCase() as 'SSH' | 'RDP' | 'VNC' | undefined;
if (!connectionType || !['SSH', 'RDP', 'VNC'].includes(connectionType)) {
throw new Error('必须提供有效的连接类型 (SSH, RDP 或 VNC)。');
}
if (!input.host) {
throw new Error('缺少必要的连接信息 (host)。');
}
const savedCredential = typeof input.login_credential_id === 'number'
? await _getSavedCredentialSnapshot(input.login_credential_id, connectionType)
: null;
let encryptedPassword = null;
let encryptedPrivateKey = null;
let encryptedPassphrase = null;
let sshKeyIdToSave: number | null = null;
let authMethodForDb: 'password' | 'key' = 'password';
let usernameToSave = input.username ?? savedCredential?.username ?? '';
let loginCredentialIdToSave: number | null = savedCredential?.id ?? null;
if (!usernameToSave) {
throw new Error('缺少必要的连接信息 (username)。');
}
if (savedCredential) {
usernameToSave = savedCredential.username;
authMethodForDb = savedCredential.auth_method;
encryptedPassword = savedCredential.encrypted_password ?? null;
encryptedPrivateKey = savedCredential.encrypted_private_key ?? null;
encryptedPassphrase = savedCredential.encrypted_passphrase ?? null;
sshKeyIdToSave = savedCredential.ssh_key_id ?? null;
} else if (connectionType === 'SSH') {
if (!input.auth_method || !['password', 'key'].includes(input.auth_method)) {
throw new Error('SSH 连接必须提供有效的认证方式 (password 或 key)。');
}
authMethodForDb = input.auth_method;
if (input.auth_method === 'password') {
if (!input.password) {
throw new Error('SSH 密码认证方式需要提供 password。');
}
encryptedPassword = encrypt(input.password!);
sshKeyIdToSave = null;
} else {
if (input.ssh_key_id) {
const keyExists = await SshKeyService.getSshKeyDbRowById(input.ssh_key_id);
if (!keyExists) {
throw new Error(`提供的 SSH 密钥 ID ${input.ssh_key_id} 无效或不存在。`);
}
sshKeyIdToSave = input.ssh_key_id;
encryptedPrivateKey = null;
encryptedPassphrase = null;
} else if (input.private_key) {
encryptedPrivateKey = encrypt(input.private_key!);
if (input.passphrase) {
encryptedPassphrase = encrypt(input.passphrase);
}
sshKeyIdToSave = null;
} else {
throw new Error('SSH 密钥认证方式内部错误:未提供 private_key 或 ssh_key_id。');
}
}
} else if (connectionType === 'RDP') {
if (!input.password) {
throw new Error('RDP 连接需要提供 password。');
}
encryptedPassword = encrypt(input.password!);
encryptedPrivateKey = null;
encryptedPassphrase = null;
sshKeyIdToSave = null;
} else {
if (!input.password) {
throw new Error('VNC 连接需要提供 password。');
}
encryptedPassword = encrypt(input.password!);
authMethodForDb = 'password';
encryptedPrivateKey = null;
encryptedPassphrase = null;
sshKeyIdToSave = null;
}
let defaultPort = 22; // Default for SSH
if (connectionType === 'RDP') {
defaultPort = 3389;
} else if (connectionType === 'VNC') {
defaultPort = 5900; // Default VNC port
}
const connectionData: ConnectionDataForRepo = {
name: input.name || '',
type: connectionType,
host: input.host,
port: input.port ?? defaultPort,
username: usernameToSave,
auth_method: authMethodForDb,
encrypted_password: encryptedPassword,
encrypted_private_key: encryptedPrivateKey,
encrypted_passphrase: encryptedPassphrase,
ssh_key_id: sshKeyIdToSave,
login_credential_id: loginCredentialIdToSave,
notes: input.notes ?? null,
proxy_id: input.proxy_id ?? null,
proxy_type: input.proxy_type ?? null,
jump_chain: processedJumpChain,
};
console.log('[Service:createConnection] Data being passed to ConnectionRepository.createConnection:', JSON.stringify(connectionData, null, 2));
const newConnectionId = await ConnectionRepository.createConnection(connectionData as Omit<ConnectionRepository.FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at' | 'tag_ids'>);
const tagIds = input.tag_ids?.filter(id => typeof id === 'number' && id > 0) ?? [];
if (tagIds.length > 0) {
await ConnectionRepository.updateConnectionTags(newConnectionId, tagIds);
}
const newConnection = await getConnectionById(newConnectionId);
if (!newConnection) {
console.error(`[Audit Log Error] Failed to retrieve connection ${newConnectionId} after creation.`);
throw new Error('创建连接后无法检索到该连接。');
}
auditLogService.logAction('CONNECTION_CREATED', { connectionId: newConnection.id, type: newConnection.type, name: newConnection.name, host: newConnection.host });
return newConnection;
};
/**
* 更新连接信息
*/
export const updateConnection = async (id: number, input: UpdateConnectionInput): Promise<ConnectionWithTags | null> => {
const currentFullConnection = await ConnectionRepository.findFullConnectionById(id);
if (!currentFullConnection) {
return null;
}
const dataToUpdate: Partial<Omit<ConnectionRepository.FullConnectionData & { ssh_key_id?: number | null; jump_chain?: number[] | null; proxy_type?: 'proxy' | 'jump' | null; login_credential_id?: number | null }, 'id' | 'created_at' | 'last_connected_at' | 'tag_ids'>> = {};
let needsCredentialUpdate = false;
const targetType = input.type?.toUpperCase() as 'SSH' | 'RDP' | 'VNC' | undefined || currentFullConnection.type;
if (currentFullConnection.login_credential_id && input.login_credential_id === undefined && targetType !== currentFullConnection.type) {
throw new Error('当前连接正在使用已保存登录凭证,切换连接类型前请先改为直填或重新选择匹配类型的登录凭证。');
}
if (input.jump_chain !== undefined || input.proxy_id !== undefined) {
const currentProxyId = input.proxy_id !== undefined ? input.proxy_id : currentFullConnection.proxy_id;
let jumpChainFromDb: number[] | null = null;
if (currentFullConnection.jump_chain) { // currentFullConnection.jump_chain is string | null
try {
jumpChainFromDb = JSON.parse(currentFullConnection.jump_chain) as number[];
} catch (e) {
console.error(`[Service:updateConnection] Failed to parse jump_chain from DB for connection ${id}: ${currentFullConnection.jump_chain}`, e);
// Treat as null if parsing fails, or consider throwing an error
jumpChainFromDb = null;
}
}
const currentJumpChainForValidation: number[] | null | undefined = input.jump_chain !== undefined ? input.jump_chain : jumpChainFromDb;
const processedJumpChain = await _validateAndProcessJumpChain(currentJumpChainForValidation, currentProxyId, id);
dataToUpdate.jump_chain = processedJumpChain;
dataToUpdate.proxy_id = currentProxyId;
}
if (input.name !== undefined) dataToUpdate.name = input.name || '';
if (input.type !== undefined && targetType !== currentFullConnection.type) dataToUpdate.type = targetType;
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.notes !== undefined) dataToUpdate.notes = input.notes;
if (input.proxy_type !== undefined) dataToUpdate.proxy_type = input.proxy_type;
if (input.login_credential_id !== undefined) {
if (input.login_credential_id === null) {
dataToUpdate.login_credential_id = null;
} else {
const savedCredential = await _getSavedCredentialSnapshot(input.login_credential_id, targetType);
dataToUpdate.login_credential_id = savedCredential.id;
dataToUpdate.username = savedCredential.username;
dataToUpdate.auth_method = savedCredential.auth_method;
dataToUpdate.encrypted_password = savedCredential.encrypted_password ?? null;
dataToUpdate.encrypted_private_key = savedCredential.encrypted_private_key ?? null;
dataToUpdate.encrypted_passphrase = savedCredential.encrypted_passphrase ?? null;
dataToUpdate.ssh_key_id = savedCredential.ssh_key_id ?? null;
needsCredentialUpdate = true;
}
}
const allowDirectCredentialEdit =
input.login_credential_id === null ||
(!currentFullConnection.login_credential_id && input.login_credential_id === undefined);
if (allowDirectCredentialEdit && targetType === 'SSH') {
const currentAuthMethod = currentFullConnection.auth_method;
const inputAuthMethod = input.auth_method;
const finalAuthMethod = inputAuthMethod || currentAuthMethod;
if (finalAuthMethod !== currentAuthMethod) {
dataToUpdate.auth_method = finalAuthMethod;
}
if (finalAuthMethod === 'password') {
if (input.password !== undefined) {
if (!input.password && finalAuthMethod !== currentAuthMethod) {
throw new Error('切换到密码认证时需要提供 password。');
}
dataToUpdate.encrypted_password = input.password ? encrypt(input.password) : null;
needsCredentialUpdate = true;
}
if (finalAuthMethod !== currentAuthMethod) {
dataToUpdate.encrypted_private_key = null;
dataToUpdate.encrypted_passphrase = null;
dataToUpdate.ssh_key_id = null;
}
} else {
if (input.ssh_key_id !== undefined) {
if (input.ssh_key_id === null) {
dataToUpdate.ssh_key_id = null;
if (input.private_key === undefined) {
dataToUpdate.encrypted_private_key = null;
dataToUpdate.encrypted_passphrase = null;
} else {
dataToUpdate.encrypted_private_key = input.private_key ? encrypt(input.private_key) : null;
dataToUpdate.encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null;
}
} else {
const keyExists = await SshKeyService.getSshKeyDbRowById(input.ssh_key_id);
if (!keyExists) {
throw new Error(`提供的 SSH 密钥 ID ${input.ssh_key_id} 无效或不存在。`);
}
dataToUpdate.ssh_key_id = input.ssh_key_id;
// Clear direct key fields when selecting a stored key
dataToUpdate.encrypted_private_key = null;
dataToUpdate.encrypted_passphrase = null;
}
needsCredentialUpdate = true;
} else if (input.private_key !== undefined) {
if (!input.private_key && finalAuthMethod !== currentAuthMethod) {
throw new Error('切换到密钥认证时需要提供 private_key 或选择一个已保存的密钥。');
}
dataToUpdate.encrypted_private_key = input.private_key ? encrypt(input.private_key) : null;
if (input.passphrase !== undefined) {
dataToUpdate.encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null;
} else if (input.private_key) {
dataToUpdate.encrypted_passphrase = null;
}
dataToUpdate.ssh_key_id = null;
needsCredentialUpdate = true;
} else if (input.passphrase !== undefined && !input.ssh_key_id && currentFullConnection.encrypted_private_key) {
dataToUpdate.encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null;
needsCredentialUpdate = true;
}
if (finalAuthMethod !== currentAuthMethod) {
dataToUpdate.encrypted_password = null;
}
}
} else if (allowDirectCredentialEdit && targetType === 'RDP') {
if (input.password !== undefined) {
dataToUpdate.encrypted_password = input.password ? encrypt(input.password) : null;
needsCredentialUpdate = true;
}
if (targetType !== currentFullConnection.type || needsCredentialUpdate || Object.keys(dataToUpdate).includes('type')) {
dataToUpdate.auth_method = 'password';
dataToUpdate.encrypted_private_key = null;
dataToUpdate.encrypted_passphrase = null;
dataToUpdate.ssh_key_id = null;
}
} else if (allowDirectCredentialEdit) {
if (input.password !== undefined) {
dataToUpdate.encrypted_password = input.password ? encrypt(input.password) : null;
needsCredentialUpdate = true;
}
if (targetType !== currentFullConnection.type || needsCredentialUpdate || Object.keys(dataToUpdate).includes('type')) {
dataToUpdate.auth_method = 'password';
dataToUpdate.encrypted_private_key = null;
dataToUpdate.encrypted_passphrase = null;
dataToUpdate.ssh_key_id = null;
}
}
const hasNonTagChanges = Object.keys(dataToUpdate).length > 0;
let updatedFieldsForAudit: string[] = [];
if (hasNonTagChanges) {
updatedFieldsForAudit = Object.keys(dataToUpdate);
console.log(`[Service:updateConnection] Data being passed to ConnectionRepository.updateConnection for ID ${id}:`, JSON.stringify(dataToUpdate, null, 2));
const updated = await ConnectionRepository.updateConnection(id, dataToUpdate);
if (!updated) {
throw new Error('更新连接记录失败。');
}
}
if (input.tag_ids !== undefined) {
const validTagIds = input.tag_ids.filter(tagId => typeof tagId === 'number' && tagId > 0);
await ConnectionRepository.updateConnectionTags(id, validTagIds);
}
if (input.tag_ids !== undefined) {
updatedFieldsForAudit.push('tag_ids');
}
if (hasNonTagChanges || input.tag_ids !== undefined) {
const auditDetails: any = { connectionId: id, updatedFields: updatedFieldsForAudit };
if (dataToUpdate.type) {
auditDetails.newType = dataToUpdate.type;
}
auditLogService.logAction('CONNECTION_UPDATED', auditDetails);
}
return getConnectionById(id);
};
/**
* 删除连接
*/
export const deleteConnection = async (id: number): Promise<boolean> => {
const deleted = await ConnectionRepository.deleteConnection(id);
if (deleted) {
// 删除成功后记录审计操作
auditLogService.logAction('CONNECTION_DELETED', { connectionId: id });
}
return deleted;
};
/**
* 获取连接信息(包含标签)以及解密后的凭证(如果适用)
* @param id 连接 ID
* @returns 包含 ConnectionWithTags 和解密后密码/密钥的对象,或 null
*/
export const getConnectionWithDecryptedCredentials = async (
id: number
): Promise<{ connection: ConnectionWithTags; decryptedPassword?: string; decryptedPrivateKey?: string; decryptedPassphrase?: string } | null> => {
// 1. 获取完整的连接数据(包含加密字段和可能的 ssh_key_id
const fullConnectionDbRow = await ConnectionRepository.findFullConnectionById(id);
if (!fullConnectionDbRow) {
console.log(`[Service:getConnWithDecrypt] Connection not found for ID: ${id}`);
return null;
}
// Convert DbRow to the stricter FullConnectionData type expected by the service/types file
// Handle potential undefined by defaulting to null
const fullConnection: FullConnectionData = {
...fullConnectionDbRow,
username: fullConnectionDbRow.login_credential_username ?? fullConnectionDbRow.username,
auth_method: fullConnectionDbRow.login_credential_auth_method ?? fullConnectionDbRow.auth_method,
encrypted_password: fullConnectionDbRow.login_credential_encrypted_password ?? fullConnectionDbRow.encrypted_password ?? null,
encrypted_private_key: fullConnectionDbRow.login_credential_encrypted_private_key ?? fullConnectionDbRow.encrypted_private_key ?? null,
encrypted_passphrase: fullConnectionDbRow.login_credential_encrypted_passphrase ?? fullConnectionDbRow.encrypted_passphrase ?? null,
ssh_key_id: fullConnectionDbRow.login_credential_ssh_key_id ?? fullConnectionDbRow.ssh_key_id ?? null,
login_credential_id: fullConnectionDbRow.login_credential_id ?? null,
} as FullConnectionData & { ssh_key_id: number | null }; // Type assertion
// 2. 获取带标签的连接数据(用于返回给调用者)
const connectionWithTags: ConnectionWithTags | null = await ConnectionRepository.findConnectionByIdWithTags(id);
if (!connectionWithTags) {
// This shouldn't happen if findFullConnectionById succeeded, but good practice to check
console.error(`[Service:getConnWithDecrypt] Mismatch: Full connection found but tagged connection not found for ID: ${id}`);
// Consider throwing an error or returning a specific error state
return null;
}
// 3. 解密凭证
let decryptedPassword: string | undefined = undefined;
let decryptedPrivateKey: string | undefined = undefined;
let decryptedPassphrase: string | undefined = undefined;
try {
// Decrypt password if method is 'password' and encrypted password exists
if (fullConnection.auth_method === 'password' && fullConnection.encrypted_password) {
decryptedPassword = decrypt(fullConnection.encrypted_password);
}
// Decrypt key and passphrase if method is 'key'
else if (fullConnection.auth_method === 'key') {
if (fullConnection.ssh_key_id) {
// +++ If using ssh_key_id, fetch and decrypt the stored key +++
console.log(`[Service:getConnWithDecrypt] Connection ${id} uses stored SSH key ID: ${fullConnection.ssh_key_id}. Fetching key...`);
const storedKeyDetails = await SshKeyService.getDecryptedSshKeyById(fullConnection.ssh_key_id);
if (!storedKeyDetails) {
// This indicates an inconsistency, as the ssh_key_id should be valid
console.error(`[Service:getConnWithDecrypt] Error: Connection ${id} references non-existent SSH key ID ${fullConnection.ssh_key_id}`);
throw new Error(`关联的 SSH 密钥 (ID: ${fullConnection.ssh_key_id}) 未找到。`);
}
decryptedPrivateKey = storedKeyDetails.privateKey;
decryptedPassphrase = storedKeyDetails.passphrase;
console.log(`[Service:getConnWithDecrypt] Successfully fetched and decrypted stored SSH key ${fullConnection.ssh_key_id} for connection ${id}.`);
} else if (fullConnection.encrypted_private_key) {
// Decrypt the key stored directly in the connection record
decryptedPrivateKey = decrypt(fullConnection.encrypted_private_key);
// Only decrypt passphrase if it exists alongside the direct key
if (fullConnection.encrypted_passphrase) {
decryptedPassphrase = decrypt(fullConnection.encrypted_passphrase);
}
} else {
console.warn(`[Service:getConnWithDecrypt] Connection ${id} uses key auth but has neither ssh_key_id nor encrypted_private_key.`);
// No key available to decrypt
}
}
} catch (error: any) { // Catch decryption or key fetching errors
console.error(`[Service:getConnWithDecrypt] Failed to decrypt credentials for connection ID ${id}:`, error);
// Decide how to handle decryption errors. Throw? Return null password?
// For now, we'll log and continue, returning undefined credentials.
// Consider throwing an error if credentials are required but decryption fails.
// Or return a specific error structure: return { error: 'Decryption failed' };
}
console.log(`[Service:getConnWithDecrypt] Returning data for ID: ${id}, Auth Method: ${fullConnection.auth_method}`);
return {
connection: connectionWithTags,
decryptedPassword,
decryptedPrivateKey,
decryptedPassphrase,
};
};
// 注意:testConnection、importConnections、exportConnections 逻辑
// 将分别移至 SshService 和 ImportExportService。
/**
* 克隆连接
* @param originalId 要克隆的原始连接 ID
* @param newName 新连接的名称
* @returns 克隆后的新连接信息(包含标签)
*/
export const cloneConnection = async (originalId: number, newName: string): Promise<ConnectionWithTags> => {
// 1. 检查新名称是否已存在
const existingByName = await ConnectionRepository.findConnectionByName(newName);
if (existingByName) {
throw new Error(`名称为 "${newName}" 的连接已存在。`);
}
// 2. 获取原始连接的完整数据(包括加密字段和 ssh_key_id
const originalFullConnection = await ConnectionRepository.findFullConnectionById(originalId);
if (!originalFullConnection) {
throw new Error(`ID 为 ${originalId} 的原始连接未找到。`);
}
// 3. 准备新连接的数据
// 使用 Omit 来排除不需要的字段,并确保类型正确
const dataForNewConnection: Omit<ConnectionRepository.FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at' | 'tag_ids'> = {
name: newName,
type: originalFullConnection.type,
host: originalFullConnection.host,
port: originalFullConnection.port,
username: originalFullConnection.username,
auth_method: originalFullConnection.auth_method,
encrypted_password: originalFullConnection.encrypted_password ?? null,
encrypted_private_key: originalFullConnection.encrypted_private_key ?? null,
encrypted_passphrase: originalFullConnection.encrypted_passphrase ?? null,
ssh_key_id: originalFullConnection.ssh_key_id ?? null, // 保留原始的 ssh_key_id
login_credential_id: originalFullConnection.login_credential_id ?? null,
proxy_id: originalFullConnection.proxy_id ?? null,
proxy_type: originalFullConnection.proxy_type ?? null, // 新增 proxy_type 复制
notes: originalFullConnection.notes ?? null, // 确保 notes 被复制
jump_chain: originalFullConnection.jump_chain ? JSON.parse(originalFullConnection.jump_chain) as number[] : null, // 复制并解析 jump_chain
// 移除不存在的 RDP 字段复制
// ...(originalFullConnection.rdp_security && { rdp_security: originalFullConnection.rdp_security }),
// ...(originalFullConnection.rdp_ignore_cert !== undefined && { rdp_ignore_cert: originalFullConnection.rdp_ignore_cert }),
};
// 4. 创建新连接记录
const newConnectionId = await ConnectionRepository.createConnection(dataForNewConnection);
// 5. 复制原始连接的标签
const originalTags = await ConnectionRepository.findConnectionTags(originalId);
if (originalTags.length > 0) {
const tagIds = originalTags.map(tag => tag.id);
await ConnectionRepository.updateConnectionTags(newConnectionId, tagIds);
}
// 6. 记录审计操作
const clonedConnection = await getConnectionById(newConnectionId);
if (!clonedConnection) {
console.error(`[Audit Log Error] Failed to retrieve connection ${newConnectionId} after cloning from ${originalId}.`);
throw new Error('克隆连接后无法检索到该连接。');
}
// 使用 CONNECTION_CREATED 事件,但添加额外信息表明是克隆操作
auditLogService.logAction('CONNECTION_CREATED', {
connectionId: clonedConnection.id,
type: clonedConnection.type,
name: clonedConnection.name,
host: clonedConnection.host,
clonedFromId: originalId // 添加克隆来源信息
});
// 7. 返回新创建的带标签的连接
return clonedConnection;
};
// 注意:updateConnectionTags 现在主要由 updateConnection 内部调用,
// 或者可以保留用于单独更新单个连接标签的场景(如果需要的话)。
// 为了解决嵌套事务问题,我们添加一个新的批量添加函数。
/**
* 为指定的一组连接添加一个标签
* @param connectionIds 连接 ID 数组
* @param tagId 要添加的标签 ID
*/
export const addTagToConnections = async (connectionIds: number[], tagId: number): Promise<void> => {
// 1. 验证 tagId 是否有效(可选,但建议)
// const tagExists = await TagRepository.findTagById(tagId); // 需要导入 TagRepository
// if (!tagExists) {
// throw new Error(`标签 ID ${tagId} 不存在。`);
// }
// 2. 调用仓库层批量添加标签
try {
await ConnectionRepository.addTagToMultipleConnections(connectionIds, tagId);
// 记录审计日志 (可以考虑为批量操作定义新的审计类型)
// TODO: 定义 'CONNECTIONS_TAG_ADDED' 审计日志类型
// auditLogService.logAction('CONNECTIONS_TAG_ADDED', { connectionIds, tagId });
} catch (error: any) {
console.error(`Service: 为连接 ${connectionIds.join(', ')} 添加标签 ${tagId} 时发生错误:`, error);
throw error; // 重新抛出错误
}
};
/**
* 更新指定连接的标签关联 (保留此函数用于可能的其他用途,但主要逻辑转移到 addTagToConnections)
* @param connectionId 连接 ID
* @param tagIds 新的标签 ID 数组
* @returns boolean 指示操作是否成功(找到连接并尝试更新)
*/
export const updateConnectionTags = async (connectionId: number, tagIds: number[]): Promise<boolean> => {
try {
const updated = await ConnectionRepository.updateConnectionTags(connectionId, tagIds);
return updated;
} catch (error: any) {
console.error(`Service: 更新连接 ${connectionId} 的标签时发生错误:`, error);
throw error;
}
};