feat: 后端: 在建立 SSH 连接时应用代理配置

This commit is contained in:
Baobhan Sith
2025-04-15 07:31:25 +08:00
parent 0e863456a2
commit 4f2f8b9f07
19 changed files with 1444 additions and 197 deletions
@@ -23,7 +23,8 @@ interface ConnectionInfoBase {
* 创建新连接 (POST /api/v1/connections)
*/
export const createConnection = async (req: Request, res: Response): Promise<void> => {
const { name, host, port = 22, username, auth_method, password, private_key, passphrase } = req.body;
// 新增 proxy_id
const { name, host, port = 22, username, auth_method, password, private_key, passphrase, proxy_id } = req.body;
const userId = req.session.userId; // 从会话获取用户 ID
// 输入验证 (基础)
@@ -67,13 +68,14 @@ export const createConnection = async (req: Request, res: Response): Promise<voi
// 插入数据库
const result = await new Promise<{ lastID: number }>((resolve, reject) => {
const stmt = db.prepare(
`INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
`INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` // 添加 proxy_id
);
// 注意:这里没有存储 userId,因为 MVP 只有一个用户。如果未来支持多用户,需要添加 user_id 字段。
stmt.run(
name, host, port, username, auth_method,
encryptedPassword, encryptedPrivateKey, encryptedPassphrase,
proxy_id ?? null, // 如果未提供则设为 null
now, now,
function (this: Statement, err: Error | null) {
if (err) {
@@ -87,11 +89,13 @@ export const createConnection = async (req: Request, res: Response): Promise<voi
});
// 返回成功响应 (不包含敏感信息)
// 返回成功响应 (包含 proxy_id)
res.status(201).json({
message: '连接创建成功。',
connection: {
id: result.lastID,
name, host, port, username, auth_method,
proxy_id: proxy_id ?? null, // 返回 proxy_id
created_at: now, updated_at: now, last_connected_at: null
}
});
@@ -111,12 +115,13 @@ export const getConnections = async (req: Request, res: Response): Promise<void>
try {
// 查询数据库,排除敏感字段 encrypted_password, encrypted_private_key, encrypted_passphrase
// 注意:如果未来支持多用户,需要添加 WHERE user_id = ? 条件
const connections = await new Promise<ConnectionInfoBase[]>((resolve, reject) => {
// 新增:包含 proxy_id
const connections = await new Promise<(ConnectionInfoBase & { proxy_id: number | null })[]>((resolve, reject) => {
db.all(
`SELECT id, name, host, port, username, auth_method, created_at, updated_at, last_connected_at
`SELECT id, name, host, port, username, auth_method, proxy_id, created_at, updated_at, last_connected_at
FROM connections
ORDER BY name ASC`, // 按名称排序
(err, rows: ConnectionInfoBase[]) => { // 使用更新后的接口
ORDER BY name ASC`,
(err, rows: (ConnectionInfoBase & { proxy_id: number | null })[]) => {
if (err) {
console.error('查询连接列表时出错:', err.message);
return reject(new Error('获取连接列表失败'));
@@ -149,13 +154,14 @@ export const getConnectionById = async (req: Request, res: Response): Promise<vo
try {
// 查询数据库,排除敏感字段
// 注意:如果未来支持多用户,需要添加 AND user_id = ? 条件
const connection = await new Promise<ConnectionInfoBase | null>((resolve, reject) => {
// 新增:包含 proxy_id
const connection = await new Promise<(ConnectionInfoBase & { proxy_id: number | null }) | null>((resolve, reject) => {
db.get(
`SELECT id, name, host, port, username, auth_method, created_at, updated_at, last_connected_at
`SELECT id, name, host, port, username, auth_method, proxy_id, created_at, updated_at, last_connected_at
FROM connections
WHERE id = ?`,
[connectionId],
(err, row: ConnectionInfoBase) => { // 使用更新后的接口
(err, row: (ConnectionInfoBase & { proxy_id: number | null })) => {
if (err) {
console.error(`查询连接 ${connectionId} 时出错:`, err.message);
return reject(new Error('获取连接信息失败'));
@@ -182,7 +188,8 @@ export const getConnectionById = async (req: Request, res: Response): Promise<vo
*/
export const updateConnection = async (req: Request, res: Response): Promise<void> => {
const connectionId = parseInt(req.params.id, 10);
const { name, host, port, username, auth_method, password, private_key, passphrase } = req.body;
// 新增 proxy_id
const { name, host, port, username, auth_method, password, private_key, passphrase, proxy_id } = req.body;
const userId = req.session.userId;
if (isNaN(connectionId)) {
@@ -191,7 +198,8 @@ export const updateConnection = async (req: Request, res: Response): Promise<voi
}
// 输入验证 (与创建类似,但允许部分更新)
if (!name && !host && port === undefined && !username && !auth_method && !password && !private_key && passphrase === undefined) {
// 更新验证逻辑以包含 proxy_id
if (!name && !host && port === undefined && !username && !auth_method && !password && !private_key && passphrase === undefined && proxy_id === undefined) {
res.status(400).json({ message: '没有提供要更新的字段。' });
return;
}
@@ -200,13 +208,37 @@ export const updateConnection = async (req: Request, res: Response): Promise<voi
return;
}
// 如果提供了 auth_method,需要确保对应的凭证也提供了或已存在
// (更复杂的验证逻辑可能需要先查询现有记录)
// (更复杂的验证逻辑可能需要先查询现有记录) - 现在实现它
try {
// 1. 先查询当前的连接信息
const currentConnection = await new Promise<(ConnectionInfoBase & { encrypted_password?: string | null, encrypted_private_key?: string | null, encrypted_passphrase?: string | null, proxy_id?: number | null }) | null>((resolve, reject) => {
// 注意:需要查询加密字段以进行比较和保留
db.get(
`SELECT id, name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id
FROM connections
WHERE id = ?`,
[connectionId],
(err, row: any) => { // 使用 any 避免类型冲突,或定义更完整的接口
if (err) {
console.error(`查询连接 ${connectionId} 时出错:`, err.message);
return reject(new Error('获取连接信息失败'));
}
resolve(row || null);
}
);
});
if (!currentConnection) {
res.status(404).json({ message: '连接未找到。' });
return;
}
const fieldsToUpdate: { [key: string]: any } = {};
const params: any[] = [];
let newAuthMethod = auth_method || currentConnection.auth_method; // 确定最终的认证方式
// 构建要更新的字段和参数
// 构建要更新的非敏感字段和参数
if (name !== undefined) { fieldsToUpdate.name = name; params.push(name); }
if (host !== undefined) { fieldsToUpdate.host = host; params.push(host); }
if (port !== undefined) {
@@ -217,55 +249,69 @@ export const updateConnection = async (req: Request, res: Response): Promise<voi
fieldsToUpdate.port = port; params.push(port);
}
if (username !== undefined) { fieldsToUpdate.username = username; params.push(username); }
// 新增:处理 proxy_id 更新 (允许设为 null)
if (proxy_id !== undefined) { fieldsToUpdate.proxy_id = proxy_id; params.push(proxy_id ?? null); }
// 处理认证方式和凭证更新
if (auth_method) {
// --- 处理认证方式和凭证更新 (重构逻辑) ---
if (auth_method && auth_method !== currentConnection.auth_method) {
// --- Case 1: 认证方式已改变 ---
fieldsToUpdate.auth_method = auth_method;
params.push(auth_method);
if (auth_method === 'password') {
// 切换到密码认证
if (!password) {
res.status(400).json({ message: '更新为密码认证时需要提供 password。' });
// 必须提供密码才能切换
res.status(400).json({ message: '切换到密码认证时需要提供 password。' });
return;
}
fieldsToUpdate.encrypted_password = encrypt(password);
params.push(fieldsToUpdate.encrypted_password);
fieldsToUpdate.encrypted_private_key = null; // 清除旧密钥
// 清除旧密钥信息
fieldsToUpdate.encrypted_private_key = null;
params.push(null);
fieldsToUpdate.encrypted_passphrase = null; // 清除旧密码
fieldsToUpdate.encrypted_passphrase = null;
params.push(null);
} else if (auth_method === 'key') {
} else { // auth_method === 'key'
// 切换到密钥认证
if (!private_key) {
res.status(400).json({ message: '更新为密钥认证时需要提供 private_key。' });
// 必须提供私钥才能切换
res.status(400).json({ message: '切换到密钥认证时需要提供 private_key。' });
return;
}
fieldsToUpdate.encrypted_private_key = encrypt(private_key);
params.push(fieldsToUpdate.encrypted_private_key);
// 密码短语是可选的
fieldsToUpdate.encrypted_passphrase = passphrase ? encrypt(passphrase) : null;
params.push(fieldsToUpdate.encrypted_passphrase);
fieldsToUpdate.encrypted_password = null; // 清除旧密码
// 清除旧密码信息
fieldsToUpdate.encrypted_password = null;
params.push(null);
}
} else {
// 如果只更新凭证而不改变 auth_method (需要先查询当前 auth_method)
// 为了简化,这里假设如果提供了 password/private_key,则 auth_method 也被提供了
// 或者,可以先查询记录再决定如何更新
if (password) {
// 假设当前是 password 方式或要切换到 password
fieldsToUpdate.encrypted_password = encrypt(password);
params.push(fieldsToUpdate.encrypted_password);
}
if (private_key) {
// 假设当前是 key 方式或要切换到 key
fieldsToUpdate.encrypted_private_key = encrypt(private_key);
params.push(fieldsToUpdate.encrypted_private_key);
fieldsToUpdate.encrypted_passphrase = passphrase ? encrypt(passphrase) : null;
params.push(fieldsToUpdate.encrypted_passphrase);
} else if (passphrase !== undefined && auth_method === 'key') { // 仅更新 passphrase
fieldsToUpdate.encrypted_passphrase = passphrase ? encrypt(passphrase) : null;
params.push(fieldsToUpdate.encrypted_passphrase);
// --- Case 2: 认证方式未改变 (或请求中未指定 auth_method) ---
// 仅当提供了新的凭证时才更新
if (currentConnection.auth_method === 'password') {
if (password) { // 如果提供了新密码
fieldsToUpdate.encrypted_password = encrypt(password);
params.push(fieldsToUpdate.encrypted_password);
}
// 如果没提供新密码,则不更新密码字段,保留旧密码
} else if (currentConnection.auth_method === 'key') {
if (private_key) { // 如果提供了新私钥
fieldsToUpdate.encrypted_private_key = encrypt(private_key);
params.push(fieldsToUpdate.encrypted_private_key);
// 如果提供了新私钥,则密码短语也必须一起更新(即使是清空)
fieldsToUpdate.encrypted_passphrase = passphrase ? encrypt(passphrase) : null;
params.push(fieldsToUpdate.encrypted_passphrase);
} else if (passphrase !== undefined) { // 如果只提供了密码短语 (允许清空)
fieldsToUpdate.encrypted_passphrase = passphrase ? encrypt(passphrase) : null;
params.push(fieldsToUpdate.encrypted_passphrase);
}
// 如果私钥和密码短语都未提供,则不更新这两个字段,保留旧值
}
}
// --- 凭证处理结束 ---
const now = Math.floor(Date.now() / 1000);
fieldsToUpdate.updated_at = now;
@@ -304,10 +350,11 @@ export const updateConnection = async (req: Request, res: Response): Promise<voi
// 获取更新后的信息(不含敏感数据)并返回
const updatedConnection = await new Promise<ConnectionInfoBase | null>((resolve, reject) => {
db.get(
`SELECT id, name, host, port, username, auth_method, created_at, updated_at, last_connected_at
// 新增:包含 proxy_id
`SELECT id, name, host, port, username, auth_method, proxy_id, created_at, updated_at, last_connected_at
FROM connections WHERE id = ?`,
[connectionId],
(err, row: ConnectionInfoBase) => err ? reject(err) : resolve(row || null)
(err, row: ConnectionInfoBase & { proxy_id: number | null }) => err ? reject(err) : resolve(row || null)
);
});
res.status(200).json({ message: '连接更新成功。', connection: updatedConnection });