feat: 后端: 在建立 SSH 连接时应用代理配置
This commit is contained in:
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user