update
This commit is contained in:
@@ -75,6 +75,21 @@ export const findAllSshKeyNames = async (): Promise<{ id: number; name: string }
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找所有 SSH 密钥的完整记录
|
||||||
|
* @returns Promise<SshKeyDbRow[]> 密钥列表
|
||||||
|
*/
|
||||||
|
export const findAllSshKeys = async (): Promise<SshKeyDbRow[]> => {
|
||||||
|
const sql = `SELECT * FROM ssh_keys ORDER BY name ASC`;
|
||||||
|
try {
|
||||||
|
const db = await getDbInstance();
|
||||||
|
const rows = await allDb<SshKeyDbRow>(db, sql);
|
||||||
|
return rows;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Repository: 查找所有 SSH 密钥记录失败:', err.message);
|
||||||
|
throw new Error(`查找所有 SSH 密钥记录失败: ${err.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新 SSH 密钥记录
|
* 更新 SSH 密钥记录
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ import * as ConnectionRepository from '../repositories/connection.repository';
|
|||||||
import * as ProxyRepository from '../repositories/proxy.repository';
|
import * as ProxyRepository from '../repositories/proxy.repository';
|
||||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||||
import { decrypt, getEncryptionKeyBuffer as getCryptoKeyBuffer } from '../utils/crypto'; // For decrypting connection details
|
import { decrypt, getEncryptionKeyBuffer as getCryptoKeyBuffer } from '../utils/crypto'; // For decrypting connection details
|
||||||
|
import { getAllDecryptedSshKeys } from '../services/ssh_key.service'; // 静态导入
|
||||||
import archiver from 'archiver';
|
import archiver from 'archiver';
|
||||||
archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted"));
|
archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted"));
|
||||||
// We might still need fs, path, os if easyzip requires writing to a temp file for password protection,
|
|
||||||
// but let's try to do it in memory first.
|
|
||||||
// import fs from 'fs';
|
|
||||||
// import path from 'path';
|
|
||||||
// import os from 'os';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -205,19 +202,13 @@ const getPlaintextConnectionsData = async (): Promise<PlaintextExportConnectionD
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出所有连接配置为一个加密的 ZIP 文件。
|
* 导出所有连接配置为一个加密的 ZIP 文件。
|
||||||
|
* @param includeSshKeys 是否包含 SSH 密钥
|
||||||
* @returns Buffer 包含加密的 ZIP 文件内容 (IV + Ciphertext + AuthTag)。
|
* @returns Buffer 包含加密的 ZIP 文件内容 (IV + Ciphertext + AuthTag)。
|
||||||
*/
|
*/
|
||||||
export const exportConnectionsAsEncryptedZip = async (): Promise<Buffer> => {
|
export const exportConnectionsAsEncryptedZip = async (includeSshKeys: boolean = false): Promise<Buffer> => {
|
||||||
try {
|
try {
|
||||||
const connections = await getPlaintextConnectionsData();
|
const connections = await getPlaintextConnectionsData();
|
||||||
const jsonContent = JSON.stringify(connections, null, 2);
|
const connectionsJsonContent = JSON.stringify(connections, null, 2);
|
||||||
|
|
||||||
// 注意:当前版本的 adm-zip 不支持在内存中设置密码
|
|
||||||
// const zipPassword = process.env.ENCRYPTION_KEY;
|
|
||||||
// if (!zipPassword || zipPassword.trim() === '') {
|
|
||||||
// console.error('错误:ENCRYPTION_KEY 环境变量未设置或为空!无法为ZIP文件设置密码。');
|
|
||||||
// throw new Error('ENCRYPTION_KEY is not set or is empty, cannot password-protect the ZIP file.');
|
|
||||||
// }
|
|
||||||
|
|
||||||
const zipPassword = process.env.ENCRYPTION_KEY;
|
const zipPassword = process.env.ENCRYPTION_KEY;
|
||||||
if (!zipPassword || zipPassword.trim() === '') {
|
if (!zipPassword || zipPassword.trim() === '') {
|
||||||
@@ -241,7 +232,13 @@ export const exportConnectionsAsEncryptedZip = async (): Promise<Buffer> => {
|
|||||||
throw new Error(`使用 archiver 创建加密 ZIP buffer 失败: ${err.message}`);
|
throw new Error(`使用 archiver 创建加密 ZIP buffer 失败: ${err.message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
archive.append(jsonContent, { name: 'connections.json' });
|
archive.append(connectionsJsonContent, { name: 'connections.json' });
|
||||||
|
|
||||||
|
if (includeSshKeys) {
|
||||||
|
const sshKeys = await getAllDecryptedSshKeys();
|
||||||
|
const sshKeysJsonContent = JSON.stringify(sshKeys, null, 2);
|
||||||
|
archive.append(sshKeysJsonContent, { name: 'ssh_keys.json' });
|
||||||
|
}
|
||||||
|
|
||||||
await archive.finalize();
|
await archive.finalize();
|
||||||
return Buffer.concat(buffer);
|
return Buffer.concat(buffer);
|
||||||
|
|||||||
@@ -180,4 +180,31 @@ export const updateSshKey = async (id: number, input: UpdateSshKeyInput): Promis
|
|||||||
export const deleteSshKey = async (id: number): Promise<boolean> => {
|
export const deleteSshKey = async (id: number): Promise<boolean> => {
|
||||||
// 注意:删除密钥前,相关的 connections 表中的 ssh_key_id 会被设为 NULL (ON DELETE SET NULL)
|
// 注意:删除密钥前,相关的 connections 表中的 ssh_key_id 会被设为 NULL (ON DELETE SET NULL)
|
||||||
return SshKeyRepository.deleteSshKey(id);
|
return SshKeyRepository.deleteSshKey(id);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 获取所有解密后的 SSH 密钥详情
|
||||||
|
* @returns Promise<DecryptedSshKeyDetails[]> 解密后的密钥详情列表
|
||||||
|
*/
|
||||||
|
export const getAllDecryptedSshKeys = async (): Promise<DecryptedSshKeyDetails[]> => {
|
||||||
|
const dbRows = await SshKeyRepository.findAllSshKeys();
|
||||||
|
const decryptedKeys: DecryptedSshKeyDetails[] = [];
|
||||||
|
|
||||||
|
for (const dbRow of dbRows) {
|
||||||
|
try {
|
||||||
|
const privateKey = decrypt(dbRow.encrypted_private_key);
|
||||||
|
const passphrase = dbRow.encrypted_passphrase ? decrypt(dbRow.encrypted_passphrase) : undefined;
|
||||||
|
decryptedKeys.push({
|
||||||
|
id: dbRow.id,
|
||||||
|
name: dbRow.name,
|
||||||
|
privateKey,
|
||||||
|
passphrase,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Service: 解密 SSH 密钥 ${dbRow.id} 失败:`, error);
|
||||||
|
// 继续处理其他密钥,不因单个密钥解密失败而中断整个过程
|
||||||
|
// 可以选择记录错误或通知管理员,但这里我们只记录日志
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptedKeys;
|
||||||
};
|
};
|
||||||
@@ -511,7 +511,7 @@ async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
async exportAllConnections(req: Request, res: Response): Promise<void> {
|
async exportAllConnections(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[控制器] 收到导出所有连接的请求。');
|
console.log('[控制器] 收到导出所有连接的请求。');
|
||||||
const encryptedZipBuffer = await exportConnectionsAsEncryptedZip();
|
const encryptedZipBuffer = await exportConnectionsAsEncryptedZip(true);
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/zip');
|
res.setHeader('Content-Type', 'application/zip');
|
||||||
res.setHeader('Content-Disposition', 'attachment; filename="nexus_connections_export.zip"');
|
res.setHeader('Content-Disposition', 'attachment; filename="nexus_connections_export.zip"');
|
||||||
|
|||||||
Reference in New Issue
Block a user