Files
nexus-terminal/packages/backend/src/services/ssh_key.service.ts
T
Baobhan Sith d37158144e update
2025-05-10 17:51:32 +08:00

210 lines
7.8 KiB
TypeScript

import * as SshKeyRepository from '../repositories/ssh_key.repository';
import { encrypt, decrypt } from '../utils/crypto';
import { SshKeyDbRow, CreateSshKeyData, UpdateSshKeyData } from '../repositories/ssh_key.repository';
// 定义 Service 层返回给 Controller 的基本密钥信息 (不含加密内容)
export interface SshKeyBasicInfo {
id: number;
name: string;
}
// 定义 Service 层创建密钥时的输入类型
export interface CreateSshKeyInput {
name: string;
private_key: string; // 明文私钥
passphrase?: string; // 明文密码短语 (可选)
}
// 定义 Service 层更新密钥时的输入类型 (名称必选,凭证可选)
export interface UpdateSshKeyInput {
name?: string; // 名称可选,但通常会提供
private_key?: string; // 明文私钥 (可选,表示要更新)
passphrase?: string; // 明文密码短语 (可选,如果提供了私钥,则此项也可能需要更新)
}
// 定义包含解密后凭证的密钥详情
export interface DecryptedSshKeyDetails extends SshKeyBasicInfo {
privateKey: string; // 解密后的私钥
passphrase?: string; // 解密后的密码短语
}
/**
* 创建新的 SSH 密钥
* @param input 包含名称和明文凭证的对象
* @returns Promise<SshKeyBasicInfo> 新创建密钥的基本信息
*/
export const createSshKey = async (input: CreateSshKeyInput): Promise<SshKeyBasicInfo> => {
// 1. 验证输入
if (!input.name || !input.private_key) {
throw new Error('必须提供密钥名称和私钥内容。');
}
// 可选:添加更严格的私钥格式验证
// 2. 加密凭证
const encrypted_private_key = encrypt(input.private_key);
const encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null;
// 3. 准备仓库数据
const dataToSave: CreateSshKeyData = {
name: input.name,
encrypted_private_key,
encrypted_passphrase,
};
// 4. 调用仓库创建记录
try {
const newId = await SshKeyRepository.createSshKey(dataToSave);
return { id: newId, name: input.name };
} catch (error: any) {
// 处理可能的 UNIQUE constraint 错误
if (error.message && error.message.includes('UNIQUE constraint failed: ssh_keys.name')) {
throw new Error(`SSH 密钥名称 "${input.name}" 已存在。`);
}
throw error; // 重新抛出其他错误
}
};
/**
* 获取所有 SSH 密钥的基本信息 (ID 和 Name)
* @returns Promise<SshKeyBasicInfo[]> 密钥列表
*/
export const getAllSshKeyNames = async (): Promise<SshKeyBasicInfo[]> => {
return SshKeyRepository.findAllSshKeyNames();
};
/**
* 根据 ID 获取 SSH 密钥的完整数据库行 (包含加密凭证)
* 供内部服务使用,例如需要解密的场景
* @param id 密钥 ID
* @returns Promise<SshKeyDbRow | null> 密钥数据库行或 null
*/
export const getSshKeyDbRowById = async (id: number): Promise<SshKeyDbRow | null> => {
return SshKeyRepository.findSshKeyById(id);
};
/**
* 根据 ID 获取解密后的 SSH 密钥详情
* @param id 密钥 ID
* @returns Promise<DecryptedSshKeyDetails | null> 解密后的密钥详情或 null
*/
export const getDecryptedSshKeyById = async (id: number): Promise<DecryptedSshKeyDetails | null> => {
const dbRow = await SshKeyRepository.findSshKeyById(id);
if (!dbRow) {
return null;
}
try {
const privateKey = decrypt(dbRow.encrypted_private_key);
const passphrase = dbRow.encrypted_passphrase ? decrypt(dbRow.encrypted_passphrase) : undefined;
return {
id: dbRow.id,
name: dbRow.name,
privateKey,
passphrase,
};
} catch (error: any) {
console.error(`Service: 解密 SSH 密钥 ${id} 失败:`, error);
// 根据策略决定是抛出错误还是返回 null/部分信息
throw new Error(`解密 SSH 密钥 ${id} 失败。`);
}
};
/**
* 更新 SSH 密钥
* @param id 要更新的密钥 ID
* @param input 包含要更新字段的对象 (明文凭证)
* @returns Promise<SshKeyBasicInfo | null> 更新后的密钥基本信息或 null (如果未找到)
*/
export const updateSshKey = async (id: number, input: UpdateSshKeyInput): Promise<SshKeyBasicInfo | null> => {
// 1. 检查密钥是否存在
const existingKey = await SshKeyRepository.findSshKeyById(id);
if (!existingKey) {
return null; // 未找到
}
// 2. 准备要更新的数据
const dataToUpdate: UpdateSshKeyData = {};
let finalName = existingKey.name; // 保留现有名称,除非输入中提供了新名称
if (input.name !== undefined) {
if (!input.name) {
throw new Error('密钥名称不能为空。');
}
dataToUpdate.name = input.name;
finalName = input.name; // 更新最终名称
}
// 只有当提供了新的私钥时,才更新私钥和密码短语
if (input.private_key !== undefined) {
if (!input.private_key) {
throw new Error('私钥内容不能为空。');
}
dataToUpdate.encrypted_private_key = encrypt(input.private_key);
// 如果更新了私钥,则密码短语也需要更新(即使是设为 null)
dataToUpdate.encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null;
} else if (input.passphrase !== undefined && existingKey.encrypted_private_key) {
// 如果只提供了密码短语,且当前存在私钥,则只更新密码短语
dataToUpdate.encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null;
}
// 3. 如果有数据需要更新,则调用仓库
if (Object.keys(dataToUpdate).length > 0) {
try {
const updated = await SshKeyRepository.updateSshKey(id, dataToUpdate);
if (!updated) {
// 理论上不应发生,因为我们已经检查过存在性
throw new Error('更新 SSH 密钥记录失败。');
}
} catch (error: any) {
// 处理可能的 UNIQUE constraint 错误
if (error.message && error.message.includes('UNIQUE constraint failed: ssh_keys.name')) {
throw new Error(`SSH 密钥名称 "${input.name}" 已存在。`);
}
throw error; // 重新抛出其他错误
}
}
// 4. 返回更新后的基本信息
return { id: id, name: finalName };
};
/**
* 删除 SSH 密钥
* @param id 要删除的密钥 ID
* @returns Promise<boolean> 是否删除成功
*/
export const deleteSshKey = async (id: number): Promise<boolean> => {
// 注意:删除密钥前,相关的 connections 表中的 ssh_key_id 会被设为 NULL (ON DELETE SET NULL)
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;
};