feat: 在连接列表右键菜单添加克隆功能

This commit is contained in:
Baobhan Sith
2025-05-02 23:10:42 +08:00
parent 2adddeace8
commit e8b759086b
9 changed files with 221 additions and 17 deletions
@@ -379,3 +379,36 @@ export const getRdpSessionToken = async (req: Request, res: Response): Promise<v
res.status(statusCode).json({ message });
}
};
/**
* 克隆连接 (POST /api/v1/connections/:id/clone)
*/
export const cloneConnection = async (req: Request, res: Response): Promise<void> => {
try {
const originalConnectionId = parseInt(req.params.id, 10);
const { name: newName } = req.body; // 从请求体获取新名称
if (isNaN(originalConnectionId)) {
res.status(400).json({ message: '无效的原始连接 ID。' });
return;
}
if (!newName || typeof newName !== 'string') {
res.status(400).json({ message: '需要提供有效的字符串类型的新连接名称 (name)。' });
return;
}
const clonedConnection = await ConnectionService.cloneConnection(originalConnectionId, newName);
res.status(201).json({ message: '连接克隆成功。', connection: clonedConnection });
} catch (error: any) {
console.error(`Controller: 克隆连接 ${req.params.id} 时发生错误:`, error);
if (error.message.includes('未找到')) {
res.status(404).json({ message: error.message });
} else if (error.message.includes('名称已存在')) {
res.status(409).json({ message: error.message }); // 409 Conflict for duplicate name
} else {
res.status(500).json({ message: error.message || '克隆连接时发生内部服务器错误。' });
}
}
};
@@ -11,7 +11,8 @@ import {
testUnsavedConnection,
exportConnections,
importConnections,
getRdpSessionToken // Import the new controller function
getRdpSessionToken, // Import the new controller function
cloneConnection // +++ Import the clone controller function +++
} from './connections.controller';
const router = Router();
@@ -80,4 +81,7 @@ router.post('/test-unsaved', testUnsavedConnection);
// POST /api/v1/connections/:id/rdp-session - Get RDP session token via backend
router.post('/:id/rdp-session', getRdpSessionToken);
// +++ POST /api/v1/connections/:id/clone - 克隆连接 +++
router.post('/:id/clone', cloneConnection);
export default router;
@@ -135,10 +135,25 @@ export const findFullConnectionById = async (id: number): Promise<FullConnection
throw new Error('获取连接详细信息失败');
}
};
/**
* 创建新连接 (不处理标签)
/**
* 根据名称查找连接 (用于检查名称是否重复)
*/
export const findConnectionByName = async (name: string): Promise<ConnectionBase | null> => {
const sql = `SELECT id, name, type, host, port, username, auth_method, proxy_id, ssh_key_id, created_at, updated_at, last_connected_at FROM connections WHERE name = ?`;
try {
const db = await getDbInstance();
const row = await getDbRow<ConnectionBase>(db, sql, [name]);
return row || null;
} catch (err: any) {
console.error(`Repository: 查询连接名称 "${name}" 时出错:`, err.message);
throw new Error('查找连接名称失败');
}
};
/**
* 创建新连接 (不处理标签)
*/
// Update input type to reflect FullConnectionData now has 'type'
export const createConnection = async (data: Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at' | 'tag_ids'>): Promise<number> => {
@@ -276,6 +291,27 @@ export const updateConnectionTags = async (connectionId: number, tagIds: number[
}
};
/**
* 查找指定连接的所有标签
* @param connectionId 连接 ID
* @returns 标签对象数组 { id: number, name: string }[]
*/
export const findConnectionTags = async (connectionId: number): Promise<{ id: number, name: string }[]> => {
const sql = `
SELECT t.id, t.name
FROM tags t
JOIN connection_tags ct ON t.id = ct.tag_id
WHERE ct.connection_id = ?`;
try {
const db = await getDbInstance();
const rows = await allDb<{ id: number, name: string }>(db, sql, [connectionId]);
return rows;
} catch (err: any) {
console.error(`Repository: 查询连接 ${connectionId} 的标签时出错:`, err.message);
throw new Error('获取连接标签失败');
}
};
/**
* 批量插入连接(用于导入)
* 注意:此函数应在事务中调用 (由调用者负责事务)
@@ -432,3 +432,71 @@ export const getConnectionWithDecryptedCredentials = async (
};
// 注意: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
proxy_id: originalFullConnection.proxy_id ?? null,
// 移除不存在的 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;
};