225 lines
9.4 KiB
TypeScript
225 lines
9.4 KiB
TypeScript
import * as ConnectionRepository from '../repositories/connection.repository';
|
|
import { encrypt, decrypt } from '../utils/crypto';
|
|
|
|
// Re-export or define types needed by the controller/service
|
|
// Ideally, these would be in a shared types file, e.g., packages/backend/src/types/connection.types.ts
|
|
// For now, let's reuse the interfaces from the repository (adjust as needed)
|
|
export interface ConnectionBase {
|
|
id: number;
|
|
name: string | null; // Allow name to be null
|
|
host: string;
|
|
port: number;
|
|
username: string;
|
|
auth_method: 'password' | 'key';
|
|
proxy_id: number | null;
|
|
created_at: number;
|
|
updated_at: number;
|
|
last_connected_at: number | null;
|
|
}
|
|
|
|
export interface ConnectionWithTags extends ConnectionBase {
|
|
tag_ids: number[];
|
|
}
|
|
|
|
// Input type for creating a connection (from controller)
|
|
export interface CreateConnectionInput {
|
|
name?: string; // Name is now optional
|
|
host: string;
|
|
port?: number; // Optional, defaults in service/repo
|
|
username: string;
|
|
auth_method: 'password' | 'key';
|
|
password?: string; // Optional depending on auth_method
|
|
private_key?: string; // Optional depending on auth_method
|
|
passphrase?: string; // Optional for key auth
|
|
proxy_id?: number | null;
|
|
tag_ids?: number[];
|
|
}
|
|
|
|
// Input type for updating a connection (from controller)
|
|
// All fields are optional except potentially auth_method related ones
|
|
export interface UpdateConnectionInput {
|
|
name?: string;
|
|
host?: string;
|
|
port?: number;
|
|
username?: string;
|
|
auth_method?: 'password' | 'key';
|
|
password?: string;
|
|
private_key?: string;
|
|
passphrase?: string; // Use undefined to signal no change, null/empty string to clear
|
|
proxy_id?: number | null;
|
|
tag_ids?: number[];
|
|
}
|
|
|
|
|
|
/**
|
|
* 获取所有连接(包含标签)
|
|
*/
|
|
export const getAllConnections = async (): Promise<ConnectionWithTags[]> => {
|
|
return ConnectionRepository.findAllConnectionsWithTags();
|
|
};
|
|
|
|
/**
|
|
* 根据 ID 获取单个连接(包含标签)
|
|
*/
|
|
export const getConnectionById = async (id: number): Promise<ConnectionWithTags | null> => {
|
|
return ConnectionRepository.findConnectionByIdWithTags(id);
|
|
};
|
|
|
|
/**
|
|
* 创建新连接
|
|
*/
|
|
export const createConnection = async (input: CreateConnectionInput): Promise<ConnectionWithTags> => {
|
|
// 1. Validate input (basic validation, more complex validation can be added)
|
|
// Removed name validation: if (!input.name || !input.host || !input.username || !input.auth_method) {
|
|
if (!input.host || !input.username || !input.auth_method) { // Validate required fields except name
|
|
throw new Error('缺少必要的连接信息 (host, username, auth_method)。');
|
|
}
|
|
if (input.auth_method === 'password' && !input.password) {
|
|
throw new Error('密码认证方式需要提供 password。');
|
|
}
|
|
if (input.auth_method === 'key' && !input.private_key) {
|
|
throw new Error('密钥认证方式需要提供 private_key。');
|
|
}
|
|
// Add more validation as needed (port range, proxy existence etc.)
|
|
|
|
// 2. Encrypt credentials
|
|
let encryptedPassword = null;
|
|
let encryptedPrivateKey = null;
|
|
let encryptedPassphrase = null;
|
|
|
|
if (input.auth_method === 'password') {
|
|
encryptedPassword = encrypt(input.password!);
|
|
} else if (input.auth_method === 'key') {
|
|
encryptedPrivateKey = encrypt(input.private_key!);
|
|
if (input.passphrase) {
|
|
encryptedPassphrase = encrypt(input.passphrase);
|
|
}
|
|
}
|
|
|
|
// 3. Prepare data for repository
|
|
const connectionData = {
|
|
name: input.name || '', // Use empty string '' if name is empty or undefined
|
|
host: input.host,
|
|
port: input.port ?? 22, // Default port
|
|
username: input.username,
|
|
auth_method: input.auth_method,
|
|
encrypted_password: encryptedPassword,
|
|
encrypted_private_key: encryptedPrivateKey,
|
|
encrypted_passphrase: encryptedPassphrase,
|
|
proxy_id: input.proxy_id ?? null,
|
|
};
|
|
|
|
// 4. Create connection record in repository
|
|
const newConnectionId = await ConnectionRepository.createConnection(connectionData);
|
|
|
|
// 5. Handle tags
|
|
const tagIds = input.tag_ids?.filter(id => typeof id === 'number' && id > 0) ?? [];
|
|
if (tagIds.length > 0) {
|
|
await ConnectionRepository.updateConnectionTags(newConnectionId, tagIds);
|
|
}
|
|
|
|
// 6. Fetch and return the newly created connection with tags
|
|
const newConnection = await getConnectionById(newConnectionId);
|
|
if (!newConnection) {
|
|
// This should ideally not happen if creation was successful
|
|
throw new Error('创建连接后无法检索到该连接。');
|
|
}
|
|
return newConnection;
|
|
};
|
|
|
|
/**
|
|
* 更新连接信息
|
|
*/
|
|
export const updateConnection = async (id: number, input: UpdateConnectionInput): Promise<ConnectionWithTags | null> => {
|
|
// 1. Fetch current connection data (including encrypted fields) to compare
|
|
const currentFullConnection = await ConnectionRepository.findFullConnectionById(id);
|
|
if (!currentFullConnection) {
|
|
return null; // Connection not found
|
|
}
|
|
|
|
// 2. Prepare data for update
|
|
const dataToUpdate: Partial<ConnectionRepository.FullConnectionData> = {};
|
|
let needsCredentialUpdate = false;
|
|
let newAuthMethod = input.auth_method || currentFullConnection.auth_method;
|
|
|
|
// Update non-credential fields
|
|
if (input.name !== undefined) dataToUpdate.name = input.name || ''; // Use empty string '' if name is empty string or null/undefined
|
|
if (input.host !== undefined) dataToUpdate.host = input.host;
|
|
if (input.port !== undefined) dataToUpdate.port = input.port;
|
|
if (input.username !== undefined) dataToUpdate.username = input.username;
|
|
if (input.proxy_id !== undefined) dataToUpdate.proxy_id = input.proxy_id; // Allows setting to null
|
|
|
|
// Handle auth method change or credential update
|
|
if (input.auth_method && input.auth_method !== currentFullConnection.auth_method) {
|
|
// Auth method changed
|
|
dataToUpdate.auth_method = input.auth_method;
|
|
needsCredentialUpdate = true;
|
|
if (input.auth_method === 'password') {
|
|
if (!input.password) throw new Error('切换到密码认证时需要提供 password。');
|
|
dataToUpdate.encrypted_password = encrypt(input.password);
|
|
dataToUpdate.encrypted_private_key = null;
|
|
dataToUpdate.encrypted_passphrase = null;
|
|
} else { // key
|
|
if (!input.private_key) throw new Error('切换到密钥认证时需要提供 private_key。');
|
|
dataToUpdate.encrypted_private_key = encrypt(input.private_key);
|
|
// Only encrypt if passphrase is a non-empty string
|
|
dataToUpdate.encrypted_passphrase = (input.passphrase && input.passphrase.trim() !== '') ? encrypt(input.passphrase) : null;
|
|
dataToUpdate.encrypted_password = null;
|
|
}
|
|
} else {
|
|
// Auth method did not change, check if credentials for the current method were provided
|
|
// Only encrypt and update if a non-empty string is provided
|
|
if (newAuthMethod === 'password' && input.password && input.password.trim() !== '') {
|
|
dataToUpdate.encrypted_password = encrypt(input.password);
|
|
needsCredentialUpdate = true;
|
|
} else if (newAuthMethod === 'key') {
|
|
let passphraseChanged = false;
|
|
if (input.private_key && input.private_key.trim() !== '') {
|
|
dataToUpdate.encrypted_private_key = encrypt(input.private_key);
|
|
// Passphrase must be updated (or cleared) if private key is updated
|
|
// Encrypt only if non-empty, otherwise set to null
|
|
dataToUpdate.encrypted_passphrase = (input.passphrase && input.passphrase.trim() !== '') ? encrypt(input.passphrase) : null;
|
|
needsCredentialUpdate = true;
|
|
passphraseChanged = true; // Mark passphrase as handled if key changed
|
|
}
|
|
// Handle case where only passphrase is changed (and key wasn't)
|
|
// Check if input.passphrase is defined (could be empty string to clear)
|
|
if (!passphraseChanged && input.passphrase !== undefined) {
|
|
// Encrypt only if non-empty, otherwise set to null
|
|
dataToUpdate.encrypted_passphrase = (input.passphrase && input.passphrase.trim() !== '') ? encrypt(input.passphrase) : null;
|
|
needsCredentialUpdate = true; // Consider this a credential update
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Update connection record if there are changes
|
|
const hasNonTagChanges = Object.keys(dataToUpdate).length > 0;
|
|
if (hasNonTagChanges) {
|
|
const updated = await ConnectionRepository.updateConnection(id, dataToUpdate);
|
|
if (!updated) {
|
|
// Should not happen if findFullConnectionById succeeded, but good practice
|
|
throw new Error('更新连接记录失败。');
|
|
}
|
|
}
|
|
|
|
// 4. Handle tags update if tag_ids were provided
|
|
if (input.tag_ids !== undefined) {
|
|
const validTagIds = input.tag_ids.filter(tagId => typeof tagId === 'number' && tagId > 0);
|
|
await ConnectionRepository.updateConnectionTags(id, validTagIds);
|
|
}
|
|
|
|
// 5. Fetch and return the updated connection
|
|
return getConnectionById(id);
|
|
};
|
|
|
|
|
|
/**
|
|
* 删除连接
|
|
*/
|
|
export const deleteConnection = async (id: number): Promise<boolean> => {
|
|
return ConnectionRepository.deleteConnection(id);
|
|
};
|
|
|
|
// Note: testConnection, importConnections, exportConnections logic
|
|
// will be moved to SshService and ImportExportService respectively.
|