This commit is contained in:
Baobhan Sith
2025-04-27 21:21:48 +08:00
parent 633c17ec67
commit b116a2b78f
11 changed files with 370 additions and 160 deletions
+1
View File
@@ -76,6 +76,7 @@ export const createConnectionsTableSQL = `
CREATE TABLE IF NOT EXISTS connections ( CREATE TABLE IF NOT EXISTS connections (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NULL, -- 允许 name 为空 name TEXT NULL, -- 允许 name 为空
type TEXT NOT NULL CHECK(type IN ('SSH', 'RDP')) DEFAULT 'SSH',
host TEXT NOT NULL, host TEXT NOT NULL,
port INTEGER NOT NULL, port INTEGER NOT NULL,
username TEXT NOT NULL, username TEXT NOT NULL,
@@ -7,6 +7,7 @@ import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/conn
interface ConnectionBase { interface ConnectionBase {
id: number; id: number;
name: string | null; name: string | null;
type: 'SSH' | 'RDP'; // Add type field
host: string; host: string;
port: number; port: number;
username: string; username: string;
@@ -17,15 +18,18 @@ interface ConnectionBase {
last_connected_at: number | null; last_connected_at: number | null;
} }
// ConnectionWithTagsRow implicitly includes 'type' via ConnectionBase
interface ConnectionWithTagsRow extends ConnectionBase { interface ConnectionWithTagsRow extends ConnectionBase {
tag_ids_str: string | null; tag_ids_str: string | null;
} }
// ConnectionWithTags implicitly includes 'type' via ConnectionBase
export interface ConnectionWithTags extends ConnectionBase { export interface ConnectionWithTags extends ConnectionBase {
tag_ids: number[]; tag_ids: number[];
} }
// 包含加密字段的完整类型,用于插入/更新 // 包含加密字段的完整类型,用于插入/更新
// FullConnectionData implicitly includes 'type' via ConnectionBase
export interface FullConnectionData extends ConnectionBase { export interface FullConnectionData extends ConnectionBase {
encrypted_password?: string | null; encrypted_password?: string | null;
encrypted_private_key?: string | null; encrypted_private_key?: string | null;
@@ -34,6 +38,7 @@ export interface FullConnectionData extends ConnectionBase {
} }
// FullConnectionDbRow implicitly includes 'type' via FullConnectionData
interface FullConnectionDbRow extends FullConnectionData { interface FullConnectionDbRow extends FullConnectionData {
proxy_db_id: number | null; proxy_db_id: number | null;
proxy_name: string | null; proxy_name: string | null;
@@ -53,7 +58,7 @@ interface FullConnectionDbRow extends FullConnectionData {
export const findAllConnectionsWithTags = async (): Promise<ConnectionWithTags[]> => { export const findAllConnectionsWithTags = async (): Promise<ConnectionWithTags[]> => {
const sql = ` const sql = `
SELECT SELECT
c.id, c.name, c.host, c.port, c.username, c.auth_method, c.proxy_id, c.id, c.name, c.type, c.host, c.port, c.username, c.auth_method, c.proxy_id,
c.created_at, c.updated_at, c.last_connected_at, c.created_at, c.updated_at, c.last_connected_at,
GROUP_CONCAT(ct.tag_id) as tag_ids_str GROUP_CONCAT(ct.tag_id) as tag_ids_str
FROM connections c FROM connections c
@@ -79,7 +84,7 @@ export const findAllConnectionsWithTags = async (): Promise<ConnectionWithTags[]
export const findConnectionByIdWithTags = async (id: number): Promise<ConnectionWithTags | null> => { export const findConnectionByIdWithTags = async (id: number): Promise<ConnectionWithTags | null> => {
const sql = ` const sql = `
SELECT SELECT
c.id, c.name, c.host, c.port, c.username, c.auth_method, c.proxy_id, c.id, c.name, c.type, c.host, c.port, c.username, c.auth_method, c.proxy_id,
c.created_at, c.updated_at, c.last_connected_at, c.created_at, c.updated_at, c.last_connected_at,
GROUP_CONCAT(ct.tag_id) as tag_ids_str GROUP_CONCAT(ct.tag_id) as tag_ids_str
FROM connections c FROM connections c
@@ -132,13 +137,15 @@ export const findFullConnectionById = async (id: number): Promise<FullConnection
/** /**
* 创建新连接 (不处理标签) * 创建新连接 (不处理标签)
*/ */
// Update input type to reflect FullConnectionData now has 'type'
export const createConnection = async (data: Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at' | 'tag_ids'>): Promise<number> => { export const createConnection = async (data: Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at' | 'tag_ids'>): Promise<number> => {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const sql = ` const sql = `
INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at) INSERT INTO connections (name, type, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; // Add type column and placeholder
const params = [ const params = [
data.name ?? null, data.name ?? null,
data.type, // Add type parameter
data.host, data.port, data.username, data.auth_method, data.host, data.port, data.username, data.auth_method,
data.encrypted_password ?? null, data.encrypted_private_key ?? null, data.encrypted_passphrase ?? null, data.encrypted_password ?? null, data.encrypted_private_key ?? null, data.encrypted_passphrase ?? null,
data.proxy_id ?? null, data.proxy_id ?? null,
@@ -160,6 +167,7 @@ export const createConnection = async (data: Omit<FullConnectionData, 'id' | 'cr
/** /**
* 更新连接信息 (不处理标签) * 更新连接信息 (不处理标签)
*/ */
// Update input type to reflect FullConnectionData now has 'type'
export const updateConnection = async (id: number, data: Partial<Omit<FullConnectionData, 'id' | 'created_at' | 'last_connected_at' | 'tag_ids'>>): Promise<boolean> => { export const updateConnection = async (id: number, data: Partial<Omit<FullConnectionData, 'id' | 'created_at' | 'last_connected_at' | 'tag_ids'>>): Promise<boolean> => {
const fieldsToUpdate: { [key: string]: any } = { ...data }; const fieldsToUpdate: { [key: string]: any } = { ...data };
const params: any[] = []; const params: any[] = [];
@@ -270,17 +278,18 @@ export const updateConnectionTags = async (connectionId: number, tagIds: number[
*/ */
export const bulkInsertConnections = async ( export const bulkInsertConnections = async (
db: Database, db: Database,
// Update input type to reflect FullConnectionData now has 'type'
connections: Array<Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { tag_ids?: number[] }> connections: Array<Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { tag_ids?: number[] }>
): Promise<{ connectionId: number, originalData: any }[]> => { ): Promise<{ connectionId: number, originalData: any }[]> => {
const insertConnSql = `INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; const insertConnSql = `INSERT INTO connections (name, type, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; // Add type column and placeholder
const results: { connectionId: number, originalData: any }[] = []; const results: { connectionId: number, originalData: any }[] = [];
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
for (const connData of connections) { for (const connData of connections) {
const params = [ const params = [
connData.name ?? null, connData.host, connData.port, connData.username, connData.auth_method, connData.name ?? null, connData.type, connData.host, connData.port, connData.username, connData.auth_method, // Add type parameter
connData.encrypted_password || null, connData.encrypted_password || null,
connData.encrypted_private_key || null, connData.encrypted_private_key || null,
connData.encrypted_passphrase || null, connData.encrypted_passphrase || null,
@@ -18,57 +18,93 @@ const auditLogService = new AuditLogService(); // 实例化 AuditLogService
* 获取所有连接(包含标签) * 获取所有连接(包含标签)
*/ */
export const getAllConnections = async (): Promise<ConnectionWithTags[]> => { export const getAllConnections = async (): Promise<ConnectionWithTags[]> => {
return ConnectionRepository.findAllConnectionsWithTags(); // Repository now returns ConnectionWithTags including 'type'
// Explicit type assertion to ensure compatibility
return ConnectionRepository.findAllConnectionsWithTags() as Promise<ConnectionWithTags[]>;
}; };
/** /**
* 根据 ID 获取单个连接(包含标签) * 根据 ID 获取单个连接(包含标签)
*/ */
export const getConnectionById = async (id: number): Promise<ConnectionWithTags | null> => { export const getConnectionById = async (id: number): Promise<ConnectionWithTags | null> => {
return ConnectionRepository.findConnectionByIdWithTags(id); // Repository now returns ConnectionWithTags including 'type'
// Explicit type assertion to ensure compatibility
return ConnectionRepository.findConnectionByIdWithTags(id) as Promise<ConnectionWithTags | null>;
}; };
/** /**
* 创建新连接 * 创建新连接
*/ */
export const createConnection = async (input: CreateConnectionInput): Promise<ConnectionWithTags> => { export const createConnection = async (input: CreateConnectionInput): Promise<ConnectionWithTags> => {
// 1. 验证输入 console.log('[Service:createConnection] Received input:', JSON.stringify(input, null, 2)); // Log input
if (!input.host || !input.username || !input.auth_method) { // 1. 验证输入 (包含 type)
throw new Error('缺少必要的连接信息 (host, username, auth_method)。'); // Convert type to uppercase for validation and consistency
const connectionType = input.type?.toUpperCase() as 'SSH' | 'RDP' | undefined; // Ensure type safety
if (!connectionType || !['SSH', 'RDP'].includes(connectionType)) {
throw new Error('必须提供有效的连接类型 (SSH 或 RDP)。');
}
if (!input.host || !input.username) {
throw new Error('缺少必要的连接信息 (host, username)。');
}
// Type-specific validation using the uppercase version
if (connectionType === 'SSH') {
if (!input.auth_method || !['password', 'key'].includes(input.auth_method)) {
throw new Error('SSH 连接必须提供有效的认证方式 (password 或 key)。');
} }
if (input.auth_method === 'password' && !input.password) { if (input.auth_method === 'password' && !input.password) {
throw new Error('密码认证方式需要提供 password。'); throw new Error('SSH 密码认证方式需要提供 password。');
} }
if (input.auth_method === 'key' && !input.private_key) { if (input.auth_method === 'key' && !input.private_key) {
throw new Error('密钥认证方式需要提供 private_key。'); throw new Error('SSH 密钥认证方式需要提供 private_key。');
}
} else if (connectionType === 'RDP') {
if (!input.password) {
throw new Error('RDP 连接需要提供 password。');
}
// For RDP, we'll ignore auth_method, private_key, passphrase from input if provided
} }
// 2. 加密凭证 // 2. 加密凭证 (根据 type)
let encryptedPassword = null; let encryptedPassword = null;
let encryptedPrivateKey = null; let encryptedPrivateKey = null;
let encryptedPassphrase = null; let encryptedPassphrase = null;
// Default to 'password' for DB compatibility, especially for RDP
let authMethodForDb: 'password' | 'key' = 'password';
if (connectionType === 'SSH') {
authMethodForDb = input.auth_method!; // Already validated above
if (input.auth_method === 'password') { if (input.auth_method === 'password') {
encryptedPassword = encrypt(input.password!); encryptedPassword = encrypt(input.password!);
} else if (input.auth_method === 'key') { } else { // key
encryptedPrivateKey = encrypt(input.private_key!); encryptedPrivateKey = encrypt(input.private_key!);
if (input.passphrase) { if (input.passphrase) {
encryptedPassphrase = encrypt(input.passphrase); encryptedPassphrase = encrypt(input.passphrase);
} }
} }
} else { // RDP (connectionType is 'RDP')
encryptedPassword = encrypt(input.password!);
// authMethodForDb remains 'password' for RDP to satisfy DB constraint
// Ensure SSH specific fields are null for RDP
encryptedPrivateKey = null;
encryptedPassphrase = null;
}
// 3. 准备仓库数据 // 3. 准备仓库数据
const connectionData = { const defaultPort = input.type === 'RDP' ? 3389 : 22;
name: input.name || '', // 如果 name 为空或 undefined,则使用空字符串 '' // Explicitly type the object being passed to the repository
const connectionData: Omit<ConnectionRepository.FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at' | 'tag_ids'> = {
name: input.name || '',
type: connectionType, // Use the validated uppercase type
host: input.host, host: input.host,
port: input.port ?? 22, // 默认端口 port: input.port ?? defaultPort, // Use type-specific default port
username: input.username, username: input.username,
auth_method: input.auth_method, auth_method: authMethodForDb, // Use determined auth method
encrypted_password: encryptedPassword, encrypted_password: encryptedPassword,
encrypted_private_key: encryptedPrivateKey, encrypted_private_key: encryptedPrivateKey, // Will be null for RDP
encrypted_passphrase: encryptedPassphrase, encrypted_passphrase: encryptedPassphrase, // Will be null for RDP
proxy_id: input.proxy_id ?? null, proxy_id: input.proxy_id ?? null,
}; };
console.log('[Service:createConnection] Data to be saved:', JSON.stringify(connectionData, null, 2)); // Log data before saving
// 4. 在仓库中创建连接记录 // 4. 在仓库中创建连接记录
const newConnectionId = await ConnectionRepository.createConnection(connectionData); const newConnectionId = await ConnectionRepository.createConnection(connectionData);
@@ -86,7 +122,7 @@ export const createConnection = async (input: CreateConnectionInput): Promise<Co
console.error(`[Audit Log Error] Failed to retrieve connection ${newConnectionId} after creation.`); console.error(`[Audit Log Error] Failed to retrieve connection ${newConnectionId} after creation.`);
throw new Error('创建连接后无法检索到该连接。'); throw new Error('创建连接后无法检索到该连接。');
} }
auditLogService.logAction('CONNECTION_CREATED', { connectionId: newConnection.id, name: newConnection.name, host: newConnection.host }); auditLogService.logAction('CONNECTION_CREATED', { connectionId: newConnection.id, type: newConnection.type, name: newConnection.name, host: newConnection.host }); // Add type to audit log
// 7. 返回新创建的带标签的连接 // 7. 返回新创建的带标签的连接
return newConnection; return newConnection;
@@ -103,57 +139,85 @@ export const updateConnection = async (id: number, input: UpdateConnectionInput)
} }
// 2. 准备更新数据 // 2. 准备更新数据
const dataToUpdate: Partial<ConnectionRepository.FullConnectionData> = {}; // Explicitly type dataToUpdate to match the repository's expected input
const dataToUpdate: Partial<Omit<ConnectionRepository.FullConnectionData, 'id' | 'created_at' | 'last_connected_at' | 'tag_ids'>> = {};
let needsCredentialUpdate = false; let needsCredentialUpdate = false;
let newAuthMethod = input.auth_method || currentFullConnection.auth_method; // Determine the final type, converting input type to uppercase if provided
const targetType = input.type?.toUpperCase() as 'SSH' | 'RDP' | undefined || currentFullConnection.type;
// 更新非凭证字段 // 更新非凭证字段
if (input.name !== undefined) dataToUpdate.name = input.name || ''; // 如果 name 是空字符串或 null/undefined,则使用空字符串 '' if (input.name !== undefined) dataToUpdate.name = input.name || '';
// Update type if changed, using the uppercase version
if (input.type !== undefined && targetType !== currentFullConnection.type) dataToUpdate.type = targetType;
if (input.host !== undefined) dataToUpdate.host = input.host; if (input.host !== undefined) dataToUpdate.host = input.host;
if (input.port !== undefined) dataToUpdate.port = input.port; if (input.port !== undefined) dataToUpdate.port = input.port;
if (input.username !== undefined) dataToUpdate.username = input.username; if (input.username !== undefined) dataToUpdate.username = input.username;
if (input.proxy_id !== undefined) dataToUpdate.proxy_id = input.proxy_id; // 允许设置为 null if (input.proxy_id !== undefined) dataToUpdate.proxy_id = input.proxy_id;
// 处理认证方法更改或凭证更新 // 处理认证方法更改或凭证更新 (根据 targetType)
if (input.auth_method && input.auth_method !== currentFullConnection.auth_method) { // Use the validated targetType for logic
// 认证方法已更改 if (targetType === 'SSH') {
dataToUpdate.auth_method = input.auth_method; const currentAuthMethod = currentFullConnection.auth_method;
const inputAuthMethod = input.auth_method;
// Determine the final auth method for SSH
const finalAuthMethod = inputAuthMethod || currentAuthMethod;
if (finalAuthMethod !== currentAuthMethod) {
dataToUpdate.auth_method = finalAuthMethod; // Update auth_method if it changed
}
if (finalAuthMethod === 'password') {
// If switching to password or updating password
if (input.password !== undefined) { // Check if password was provided in input
if (!input.password && finalAuthMethod !== currentAuthMethod) {
// Switching to password requires a password
throw new Error('切换到密码认证时需要提供 password。');
}
// Encrypt if password is not empty, otherwise set to null (to clear)
dataToUpdate.encrypted_password = input.password ? encrypt(input.password) : null;
needsCredentialUpdate = true; needsCredentialUpdate = true;
if (input.auth_method === 'password') { }
if (!input.password) throw new Error('切换到密码认证时需要提供 password。'); // When switching to password, clear key fields
dataToUpdate.encrypted_password = encrypt(input.password); if (finalAuthMethod !== currentAuthMethod) {
dataToUpdate.encrypted_private_key = null; dataToUpdate.encrypted_private_key = null;
dataToUpdate.encrypted_passphrase = null; dataToUpdate.encrypted_passphrase = null;
} else { // 密钥 }
if (!input.private_key) throw new Error('切换到密钥认证时需要提供 private_key。'); } else { // finalAuthMethod is 'key'
dataToUpdate.encrypted_private_key = encrypt(input.private_key); let keyUpdated = false;
// 仅当密码短语为非空字符串时才加密 // If switching to key or updating key
dataToUpdate.encrypted_passphrase = (input.passphrase && input.passphrase.trim() !== '') ? encrypt(input.passphrase) : null; if (input.private_key !== undefined) {
if (!input.private_key && finalAuthMethod !== currentAuthMethod) {
// Switching to key requires a private key
throw new Error('切换到密钥认证时需要提供 private_key。');
}
// Encrypt if key is not empty, otherwise set to null (to clear)
dataToUpdate.encrypted_private_key = input.private_key ? encrypt(input.private_key) : null;
needsCredentialUpdate = true;
keyUpdated = true;
}
// Update passphrase only if key was updated OR passphrase itself was provided
if (keyUpdated || input.passphrase !== undefined) {
// Encrypt if passphrase is not empty, otherwise set to null (to clear)
dataToUpdate.encrypted_passphrase = input.passphrase ? encrypt(input.passphrase) : null;
needsCredentialUpdate = true; // Consider passphrase change a credential update
}
// When switching to key, clear password field
if (finalAuthMethod !== currentAuthMethod) {
dataToUpdate.encrypted_password = null; dataToUpdate.encrypted_password = null;
} }
} else {
// 认证方法未更改,检查是否提供了当前方法的凭证
// 仅当提供了非空字符串时才加密和更新
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);
// 如果私钥更新,则必须更新(或清除)密码短语
// 仅当非空时加密,否则设置为 null
dataToUpdate.encrypted_passphrase = (input.passphrase && input.passphrase.trim() !== '') ? encrypt(input.passphrase) : null;
needsCredentialUpdate = true;
passphraseChanged = true; // 如果密钥更改,则将密码短语标记为已处理
} }
// 处理仅更改密码短语(且密钥未更改)的情况 } else { // targetType is 'RDP'
// 检查 input.passphrase 是否已定义(可能是空字符串以清除) // RDP only uses password
if (!passphraseChanged && input.passphrase !== undefined) { if (input.password !== undefined) { // Check if password was provided
// 仅当非空时加密,否则设置为 null // Encrypt if password is not empty, otherwise set to null (to clear)
dataToUpdate.encrypted_passphrase = (input.passphrase && input.passphrase.trim() !== '') ? encrypt(input.passphrase) : null; dataToUpdate.encrypted_password = input.password ? encrypt(input.password) : null;
needsCredentialUpdate = true; // 将此视为凭证更新 needsCredentialUpdate = true;
} }
// Ensure SSH specific fields are nullified if switching to RDP or updating RDP
if (targetType !== currentFullConnection.type || needsCredentialUpdate) {
dataToUpdate.auth_method = 'password'; // RDP uses password auth method in DB
dataToUpdate.encrypted_private_key = null;
dataToUpdate.encrypted_passphrase = null;
} }
} }
@@ -182,7 +246,12 @@ export const updateConnection = async (id: number, input: UpdateConnectionInput)
// 5. 如果进行了任何更改,则记录审计操作 // 5. 如果进行了任何更改,则记录审计操作
if (hasNonTagChanges || input.tag_ids !== undefined) { if (hasNonTagChanges || input.tag_ids !== undefined) {
auditLogService.logAction('CONNECTION_UPDATED', { connectionId: id, updatedFields: updatedFieldsForAudit }); // Add type to audit log if it was updated
const auditDetails: any = { connectionId: id, updatedFields: updatedFieldsForAudit };
if (dataToUpdate.type) {
auditDetails.newType = dataToUpdate.type;
}
auditLogService.logAction('CONNECTION_UPDATED', auditDetails);
} }
// 6. 获取并返回更新后的连接 // 6. 获取并返回更新后的连接
@@ -7,6 +7,7 @@ import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/conn
interface ImportedConnectionData { interface ImportedConnectionData {
name: string; name: string;
type: 'SSH' | 'RDP'; // Add type field
host: string; host: string;
port: number; port: number;
username: string; username: string;
@@ -42,10 +43,11 @@ export const exportConnections = async (): Promise<ExportedConnectionData[]> =>
try { try {
const db = await getDbInstance(); const db = await getDbInstance();
// Ensure ExportRow reflects the updated FullConnectionData (which now includes 'type')
type ExportRow = ConnectionRepository.FullConnectionData & { type ExportRow = ConnectionRepository.FullConnectionData & {
proxy_db_id: number | null; proxy_db_id: number | null;
proxy_name: string | null; proxy_name: string | null;
proxy_type: 'SOCKS5' | 'HTTP' | null; proxy_type: 'SOCKS5' | 'HTTP' | null; // Proxy type remains the same
proxy_host: string | null; proxy_host: string | null;
proxy_port: number | null; proxy_port: number | null;
proxy_username: string | null; proxy_username: string | null;
@@ -86,6 +88,7 @@ export const exportConnections = async (): Promise<ExportedConnectionData[]> =>
const formattedData: ExportedConnectionData[] = connectionsWithProxies.map(row => { const formattedData: ExportedConnectionData[] = connectionsWithProxies.map(row => {
const connection: ExportedConnectionData = { const connection: ExportedConnectionData = {
name: row.name ?? 'Unnamed', name: row.name ?? 'Unnamed',
type: row.type, // Add type field
host: row.host, host: row.host,
port: row.port, port: row.port,
username: row.username, username: row.username,
@@ -154,9 +157,18 @@ export const importConnections = async (fileBuffer: Buffer): Promise<ImportResul
for (const connData of importedData) { for (const connData of importedData) {
try { try {
if (!connData.name || !connData.host || !connData.port || !connData.username || !connData.auth_method) { // Validate imported data, including type
throw new Error('缺少必要的连接字段 (name, host, port, username, auth_method)。'); if (!connData.type || !['SSH', 'RDP'].includes(connData.type)) {
throw new Error('缺少或无效的连接类型 (type)。');
} }
if (!connData.name || !connData.host || !connData.port || !connData.username) {
throw new Error('缺少必要的连接字段 (name, host, port, username)。');
}
// Validate SSH specific fields only if type is SSH
if (connData.type === 'SSH' && (!connData.auth_method || !['password', 'key'].includes(connData.auth_method))) {
throw new Error('SSH 连接缺少有效的认证方式 (auth_method)。');
}
// RDP specific validation (e.g., password required) could be added here if needed
let proxyIdToUse: number | null = null; let proxyIdToUse: number | null = null;
@@ -192,12 +204,15 @@ export const importConnections = async (fileBuffer: Buffer): Promise<ImportResul
} }
} }
// Prepare data for repository, ensuring correct auth_method for RDP
const authMethodForDb = connData.type === 'RDP' ? 'password' : connData.auth_method!;
connectionsToInsert.push({ connectionsToInsert.push({
name: connData.name, name: connData.name,
type: connData.type, // Add type
host: connData.host, host: connData.host,
port: connData.port, port: connData.port,
username: connData.username, username: connData.username,
auth_method: connData.auth_method, auth_method: authMethodForDb, // Use determined auth method
encrypted_password: connData.encrypted_password || null, encrypted_password: connData.encrypted_password || null,
encrypted_private_key: connData.encrypted_private_key || null, encrypted_private_key: connData.encrypted_private_key || null,
encrypted_passphrase: connData.encrypted_passphrase || null, encrypted_passphrase: connData.encrypted_passphrase || null,
@@ -3,6 +3,7 @@
export interface ConnectionBase { export interface ConnectionBase {
id: number; id: number;
name: string | null; name: string | null;
type: 'SSH' | 'RDP';
host: string; host: string;
port: number; port: number;
username: string; username: string;
@@ -20,6 +21,7 @@ export interface ConnectionWithTags extends ConnectionBase {
export interface CreateConnectionInput { export interface CreateConnectionInput {
name?: string; name?: string;
type: 'SSH' | 'RDP';
host: string; host: string;
port?: number; port?: number;
username: string; username: string;
@@ -34,6 +36,7 @@ export interface CreateConnectionInput {
export interface UpdateConnectionInput { export interface UpdateConnectionInput {
name?: string; name?: string;
type?: 'SSH' | 'RDP';
host?: string; host?: string;
port?: number; port?: number;
username?: string; username?: string;
@@ -49,6 +52,7 @@ export interface UpdateConnectionInput {
export interface FullConnectionData { export interface FullConnectionData {
id: number; id: number;
name: string | null; name: string | null;
type: 'SSH' | 'RDP';
host: string; host: string;
port: number; port: number;
username: string; username: string;
@@ -26,16 +26,18 @@ const { tags, isLoading: isTagLoading, error: tagStoreError } = storeToRefs(tags
// 表单数据模型 // 表单数据模型
const initialFormData = { const initialFormData = {
type: 'SSH' as 'SSH' | 'RDP', // Use uppercase to match ConnectionInfo
name: '', name: '',
host: '', host: '',
port: 22, port: 22,
username: '', username: '',
auth_method: 'password' as 'password' | 'key', auth_method: 'password' as 'password' | 'key', // SSH specific
password: '', password: '',
private_key: '', private_key: '', // SSH specific
passphrase: '', passphrase: '', // SSH specific
proxy_id: null as number | null, proxy_id: null as number | null,
tag_ids: [] as number[], // 新增 tag_ids 字段 tag_ids: [] as number[], // 新增 tag_ids 字段
// Add RDP specific fields later if needed, e.g., domain
}; };
const formData = reactive({ ...initialFormData }); const formData = reactive({ ...initialFormData });
@@ -71,6 +73,7 @@ watch(() => props.connectionToEdit, (newVal) => {
formError.value = null; // 清除错误 formError.value = null; // 清除错误
if (newVal) { if (newVal) {
// 编辑模式:填充表单,但不填充敏感信息 // 编辑模式:填充表单,但不填充敏感信息
formData.type = newVal.type; // Correctly set the type for editing
formData.name = newVal.name; formData.name = newVal.name;
formData.host = newVal.host; formData.host = newVal.host;
formData.port = newVal.port; formData.port = newVal.port;
@@ -94,6 +97,21 @@ onMounted(() => {
tagsStore.fetchTags(); // 获取标签列表 tagsStore.fetchTags(); // 获取标签列表
}); });
// 监听连接类型变化,动态调整默认端口
watch(() => formData.type, (newType) => {
// Use uppercase for comparison
if (newType === 'RDP' && formData.port === 22) {
formData.port = 3389; // RDP 默认端口
} else if (newType === 'SSH' && formData.port === 3389) {
formData.port = 22; // SSH 默认端口
}
// 重置或调整认证方式等逻辑可以在这里添加
if (newType === 'RDP') {
// RDP 通常只用密码,可以强制或隐藏 auth_method
// formData.auth_method = 'password'; // Example: Force password for RDP
}
});
// 处理表单提交 // 处理表单提交
const handleSubmit = async () => { const handleSubmit = async () => {
formError.value = null; formError.value = null;
@@ -110,7 +128,10 @@ const handleSubmit = async () => {
return; return;
} }
// --- 更新后的验证逻辑 --- // --- 更新后的验证逻辑 (区分 SSH 和 RDP) ---
// Use uppercase for comparison
if (formData.type === 'SSH') {
// SSH Validation
// 1. 添加模式下,密码/密钥是必填的 // 1. 添加模式下,密码/密钥是必填的
if (!isEditMode.value) { if (!isEditMode.value) {
if (formData.auth_method === 'password' && !formData.password) { if (formData.auth_method === 'password' && !formData.password) {
@@ -125,8 +146,9 @@ const handleSubmit = async () => {
// 2. 编辑模式下,如果切换到密码认证,则密码必填 // 2. 编辑模式下,如果切换到密码认证,则密码必填
else if (isEditMode.value && formData.auth_method === 'password' && !formData.password) { else if (isEditMode.value && formData.auth_method === 'password' && !formData.password) {
// 检查原始连接的认证方式,如果原始不是密码,则切换时必须提供密码 // 检查原始连接的认证方式,如果原始不是密码,则切换时必须提供密码
// 注意: props.connectionToEdit 可能没有 type 字段,需要后端配合或前端自行判断
if (props.connectionToEdit?.auth_method !== 'password') { if (props.connectionToEdit?.auth_method !== 'password') {
formError.value = t('connections.form.errorPasswordRequiredOnSwitch'); // 新增翻译键 formError.value = t('connections.form.errorPasswordRequiredOnSwitch');
return; return;
} }
// 如果原始就是密码,编辑时密码可以不填(表示不修改) // 如果原始就是密码,编辑时密码可以不填(表示不修改)
@@ -135,45 +157,78 @@ const handleSubmit = async () => {
else if (isEditMode.value && formData.auth_method === 'key' && !formData.private_key) { else if (isEditMode.value && formData.auth_method === 'key' && !formData.private_key) {
// 检查原始连接的认证方式,如果原始不是密钥,则切换时必须提供私钥 // 检查原始连接的认证方式,如果原始不是密钥,则切换时必须提供私钥
if (props.connectionToEdit?.auth_method !== 'key') { if (props.connectionToEdit?.auth_method !== 'key') {
formError.value = t('connections.form.errorPrivateKeyRequiredOnSwitch'); // 新增翻译键 formError.value = t('connections.form.errorPrivateKeyRequiredOnSwitch');
return; return;
} }
// 如果原始就是密钥,编辑时私钥可以不填(表示不修改) // 如果原始就是密钥,编辑时私钥可以不填(表示不修改)
} }
// Use uppercase for comparison
} else if (formData.type === 'RDP') {
// RDP Validation
// 1. 添加模式下,密码是必填的
if (!isEditMode.value && !formData.password) {
formError.value = t('connections.form.errorPasswordRequired'); // 可以复用密码必填的翻译
return;
}
// 2. 编辑模式下,密码可以不填(表示不修改),除非是从非 RDP 类型切换过来(这个逻辑比较复杂,暂时简化为密码非必填)
// 如果需要更严格的验证(例如从 SSH 编辑为 RDP 时强制要求输入密码),可以在这里添加
}
// --- 验证逻辑结束 --- // --- 验证逻辑结束 ---
// 构建要发送的数据 (区分添加和编辑) // 构建要发送的数据 (区分添加和编辑)
const dataToSend: any = { const dataToSend: any = {
type: formData.type, // 发送连接类型
name: formData.name, name: formData.name,
host: formData.host, host: formData.host,
port: formData.port, port: formData.port,
username: formData.username, username: formData.username,
auth_method: formData.auth_method,
proxy_id: formData.proxy_id || null, proxy_id: formData.proxy_id || null,
tag_ids: formData.tag_ids || [], // 发送 tag_ids tag_ids: formData.tag_ids || [], // 发送 tag_ids
// domain: formData.domain, // 如果添加了 domain 字段
}; };
// 处理敏感字段 // 处理认证相关字段 (根据类型)
// Use uppercase for comparison
if (formData.type === 'SSH') {
dataToSend.auth_method = formData.auth_method;
if (formData.auth_method === 'password') { if (formData.auth_method === 'password') {
// 仅当用户输入新密码或在编辑模式下明确清空时才发送 // SSH 密码处理
if (formData.password) { if (formData.password) {
dataToSend.password = formData.password; dataToSend.password = formData.password;
} else if (isEditMode.value && formData.password === '') { } else if (isEditMode.value && formData.password === '') {
dataToSend.password = null; // 发送 null 表示清空密码 (后端需要能处理 null) // 编辑模式下,空密码字符串可能表示清空或不修改,取决于后端实现
// 假设发送 null 表示清空 (如果后端支持)
// dataToSend.password = null;
// 或者不发送 password 字段表示不修改
} }
} else if (formData.auth_method === 'key') { } else if (formData.auth_method === 'key') {
// 仅当用户输入新私钥时才发送 // SSH 密钥处理
if (formData.private_key) { if (formData.private_key) {
dataToSend.private_key = formData.private_key; dataToSend.private_key = formData.private_key;
} }
// 仅当用户输入新密码短语或在编辑模式下明确清空时才发送 // SSH 密码短语处理
if (formData.passphrase) { if (formData.passphrase) {
dataToSend.passphrase = formData.passphrase; dataToSend.passphrase = formData.passphrase;
} else if (isEditMode.value && formData.passphrase === '') { } else if (isEditMode.value && formData.passphrase === '') {
dataToSend.passphrase = null; // 发送 null 表示清空密码短语 // dataToSend.passphrase = null; // 发送 null 表示清空
} }
} }
// Use uppercase for comparison
} else if (formData.type === 'RDP') {
// RDP 密码处理 (通常 RDP 没有 auth_method 选择)
if (formData.password) {
dataToSend.password = formData.password;
} else if (isEditMode.value && formData.password === '') {
// 编辑 RDP 时,空密码字符串处理逻辑同上
// dataToSend.password = null;
}
// RDP 不发送 SSH 特有的字段
delete dataToSend.auth_method;
delete dataToSend.private_key;
delete dataToSend.passphrase;
}
let success = false; let success = false;
if (isEditMode.value && props.connectionToEdit) { if (isEditMode.value && props.connectionToEdit) {
@@ -300,6 +355,16 @@ const testButtonText = computed(() => {
<input type="text" id="conn-name" v-model="formData.name" <input type="text" id="conn-name" v-model="formData.name"
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" /> class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
</div> </div>
<!-- Connection Type -->
<div>
<label for="conn-type" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.connectionType', '连接类型') }}</label>
<select id="conn-type" v-model="formData.type"
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary appearance-none bg-no-repeat bg-right pr-8"
style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%236c757d\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M2 5l6 6 6-6\'/%3e%3c/svg%3e'); background-position: right 0.75rem center; background-size: 16px 12px;">
<option value="SSH">{{ t('connections.form.typeSsh', 'SSH') }}</option>
<option value="RDP">{{ t('connections.form.typeRdp', 'RDP') }}</option>
</select>
</div>
<!-- Host and Port Row --> <!-- Host and Port Row -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="md:col-span-2"> <div class="md:col-span-2">
@@ -317,12 +382,16 @@ const testButtonText = computed(() => {
<!-- Authentication Section --> <!-- Authentication Section -->
<div class="space-y-4 p-4 border border-border rounded-md bg-header/30"> <div class="space-y-4 p-4 border border-border rounded-md bg-header/30">
<h4 class="text-base font-semibold mb-3 pb-2 border-b border-border/50">{{ t('connections.form.sectionAuth', '认证方式') }}</h4> <h4 class="text-base font-semibold mb-3 pb-2 border-b border-border/50">{{ t('connections.form.sectionAuth', '认证信息') }}</h4>
<div> <div>
<label for="conn-username" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.username') }}</label> <label for="conn-username" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.username') }}</label>
<input type="text" id="conn-username" v-model="formData.username" required <input type="text" id="conn-username" v-model="formData.username" required
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" /> class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
</div> </div>
<!-- SSH Specific Auth -->
<!-- Use uppercase for comparison -->
<template v-if="formData.type === 'SSH'">
<div> <div>
<label for="conn-auth-method" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.authMethod') }}</label> <label for="conn-auth-method" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.authMethod') }}</label>
<select id="conn-auth-method" v-model="formData.auth_method" <select id="conn-auth-method" v-model="formData.auth_method"
@@ -354,6 +423,23 @@ const testButtonText = computed(() => {
<small class="block text-xs text-text-secondary">{{ t('connections.form.keyUpdateNote') }}</small> <small class="block text-xs text-text-secondary">{{ t('connections.form.keyUpdateNote') }}</small>
</div> </div>
</div> </div>
</template>
<!-- RDP Specific Auth (Password only for now) -->
<!-- Use uppercase for comparison -->
<template v-if="formData.type === 'RDP'">
<div>
<label for="conn-password-rdp" class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.password') }}</label>
<input type="password" id="conn-password-rdp" v-model="formData.password" :required="!isEditMode" autocomplete="new-password"
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
<!-- Add domain field if needed -->
<!--
<label for="conn-domain" class="block text-sm font-medium text-text-secondary mb-1 mt-4">{{ t('connections.form.domain', '域') }} ({{ t('connections.form.optional') }})</label>
<input type="text" id="conn-domain" v-model="formData.domain"
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" />
-->
</div>
</template>
</div> </div>
<!-- Advanced Options Section --> <!-- Advanced Options Section -->
@@ -388,7 +474,9 @@ const testButtonText = computed(() => {
<!-- Form Actions --> <!-- Form Actions -->
<div class="flex justify-between items-center pt-5 mt-6 flex-shrink-0"> <div class="flex justify-between items-center pt-5 mt-6 flex-shrink-0">
<div class="flex flex-col items-start gap-1"> <!-- Test Area --> <!-- Test Area (Only show for SSH) -->
<!-- Use uppercase for comparison -->
<div v-if="formData.type === 'SSH'" class="flex flex-col items-start gap-1">
<div class="flex items-center gap-2"> <!-- Button and Icon --> <div class="flex items-center gap-2"> <!-- Button and Icon -->
<button type="button" @click="handleTestConnection" :disabled="isLoading || testStatus === 'testing'" <button type="button" @click="handleTestConnection" :disabled="isLoading || testStatus === 'testing'"
class="px-3 py-1.5 border border-border rounded-md text-sm font-medium text-text-secondary bg-background hover:bg-border focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center transition-colors duration-150"> class="px-3 py-1.5 border border-border rounded-md text-sm font-medium text-text-secondary bg-background hover:bg-border focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center transition-colors duration-150">
@@ -418,12 +506,14 @@ const testButtonText = computed(() => {
</div> </div>
</div> </div>
</div> </div>
<!-- Placeholder for alignment when test button is hidden -->
<div v-else class="flex-1"></div>
<div class="flex space-x-3"> <!-- Main Actions --> <div class="flex space-x-3"> <!-- Main Actions -->
<button type="submit" @click="handleSubmit" :disabled="isLoading || testStatus === 'testing'" <button type="submit" @click="handleSubmit" :disabled="isLoading || (formData.type === 'SSH' && testStatus === 'testing')"
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out"> class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out">
{{ submitButtonText }} {{ submitButtonText }}
</button> </button>
<button type="button" @click="emit('close')" :disabled="isLoading || testStatus === 'testing'" <button type="button" @click="emit('close')" :disabled="isLoading || (formData.type === 'SSH' && testStatus === 'testing')"
class="px-4 py-2 bg-transparent text-text-secondary border border-border rounded-md shadow-sm hover:bg-border hover:text-foreground focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out"> class="px-4 py-2 bg-transparent text-text-secondary border border-border rounded-md shadow-sm hover:bg-border hover:text-foreground focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out">
{{ t('connections.form.cancel') }} {{ t('connections.form.cancel') }}
</button> </button>
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, computed, ref, reactive } from 'vue'; // 统一导入 import { onMounted, computed, ref, reactive, watch } from 'vue'; // 统一导入, 添加 watch
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -30,6 +30,11 @@ onMounted(() => {
tagsStore.fetchTags(); // 获取标签列表 tagsStore.fetchTags(); // 获取标签列表
}); });
// Log received props for debugging
watch(() => props.connections, (newVal: ConnectionInfo[]) => { // Add type annotation for newVal
console.log('[ConnectionList] Received connections prop:', JSON.stringify(newVal, null, 2));
}, { immediate: true, deep: true });
// 创建标签 ID 到名称的映射 // 创建标签 ID 到名称的映射
const tagMap = computed(() => { const tagMap = computed(() => {
const map = new Map<number, string>(); const map = new Map<number, string>();
@@ -176,7 +181,10 @@ const handleDelete = async (conn: ConnectionInfo) => {
</thead> </thead>
<tbody class="divide-y divide-border"> <tbody class="divide-y divide-border">
<tr v-for="conn in groupConnections" :key="conn.id" class="hover:bg-hover transition-colors duration-150"> <tr v-for="conn in groupConnections" :key="conn.id" class="hover:bg-hover transition-colors duration-150">
<td class="px-4 py-3 text-sm text-foreground whitespace-nowrap">{{ conn.name }}</td> <td class="px-4 py-3 text-sm text-foreground whitespace-nowrap">
<!-- Icon logic removed for now -->
{{ conn.name }}
</td>
<td class="px-4 py-3 text-sm text-foreground whitespace-nowrap">{{ conn.host }}</td> <td class="px-4 py-3 text-sm text-foreground whitespace-nowrap">{{ conn.host }}</td>
<td class="px-4 py-3 text-sm text-foreground whitespace-nowrap">{{ conn.port }}</td> <td class="px-4 py-3 text-sm text-foreground whitespace-nowrap">{{ conn.port }}</td>
<td class="px-4 py-3 text-sm text-foreground whitespace-nowrap">{{ conn.username }}</td> <td class="px-4 py-3 text-sm text-foreground whitespace-nowrap">{{ conn.username }}</td>
+3
View File
@@ -159,6 +159,9 @@
"proxy": "Proxy:", "proxy": "Proxy:",
"noProxy": "No Proxy", "noProxy": "No Proxy",
"tags": "Tags:", "tags": "Tags:",
"connectionType": "Connection Type:",
"typeSsh": "SSH",
"typeRdp": "RDP",
"sectionBasic": "Basic Information", "sectionBasic": "Basic Information",
"sectionAuth": "Authentication", "sectionAuth": "Authentication",
"sectionAdvanced": "Advanced Options", "sectionAdvanced": "Advanced Options",
+3
View File
@@ -159,6 +159,9 @@
"proxy": "プロキシ:", "proxy": "プロキシ:",
"noProxy": "プロキシなし", "noProxy": "プロキシなし",
"tags": "タグ:", "tags": "タグ:",
"connectionType": "接続タイプ:",
"typeSsh": "SSH",
"typeRdp": "RDP",
"sectionBasic": "基本情報", "sectionBasic": "基本情報",
"sectionAuth": "認証情報", "sectionAuth": "認証情報",
"sectionAdvanced": "詳細設定", "sectionAdvanced": "詳細設定",
+3
View File
@@ -159,6 +159,9 @@
"proxy": "代理:", "proxy": "代理:",
"noProxy": "无代理", "noProxy": "无代理",
"tags": "标签:", "tags": "标签:",
"connectionType": "连接类型",
"typeSsh": "SSH",
"typeRdp": "RDP",
"sectionBasic": "基本信息", "sectionBasic": "基本信息",
"sectionAuth": "认证信息", "sectionAuth": "认证信息",
"sectionAdvanced": "高级选项", "sectionAdvanced": "高级选项",
@@ -5,6 +5,7 @@ import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
export interface ConnectionInfo { export interface ConnectionInfo {
id: number; id: number;
name: string; name: string;
type: 'SSH' | 'RDP'; // Use uppercase to match backend data
host: string; host: string;
port: number; port: number;
username: string; username: string;
@@ -59,6 +60,7 @@ export const useConnectionsStore = defineStore('connections', {
console.log('[ConnectionsStore] Fetching latest connections from server...'); console.log('[ConnectionsStore] Fetching latest connections from server...');
const response = await apiClient.get<ConnectionInfo[]>('/connections'); const response = await apiClient.get<ConnectionInfo[]>('/connections');
const freshData = response.data; const freshData = response.data;
console.log('[ConnectionsStore] Data received from API:', JSON.stringify(freshData, null, 2)); // Log received data
const freshDataString = JSON.stringify(freshData); const freshDataString = JSON.stringify(freshData);
// 3. 对比并更新 // 3. 对比并更新
@@ -66,6 +68,7 @@ export const useConnectionsStore = defineStore('connections', {
if (currentDataString !== freshDataString) { if (currentDataString !== freshDataString) {
console.log('[ConnectionsStore] Connections data changed, updating state and cache.'); console.log('[ConnectionsStore] Connections data changed, updating state and cache.');
this.connections = freshData; this.connections = freshData;
console.log('[ConnectionsStore] State updated with fresh data:', JSON.stringify(this.connections, null, 2)); // Log state after update
localStorage.setItem(cacheKey, freshDataString); // 更新缓存 localStorage.setItem(cacheKey, freshDataString); // 更新缓存
} else { } else {
console.log('[ConnectionsStore] Connections data is up-to-date.'); console.log('[ConnectionsStore] Connections data is up-to-date.');
@@ -88,6 +91,7 @@ export const useConnectionsStore = defineStore('connections', {
// 更新参数类型以接受新的认证字段 // 更新参数类型以接受新的认证字段
async addConnection(newConnectionData: { async addConnection(newConnectionData: {
name: string; name: string;
type: 'SSH' | 'RDP'; // Use uppercase
host: string; host: string;
port: number; port: number;
username: string; username: string;
@@ -122,7 +126,8 @@ export const useConnectionsStore = defineStore('connections', {
// 更新连接 Action // 更新连接 Action
// 更新参数类型以包含 proxy_id 和 tag_ids // 更新参数类型以包含 proxy_id 和 tag_ids
async updateConnection(connectionId: number, updatedData: Partial<Omit<ConnectionInfo, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { password?: string; private_key?: string; passphrase?: string; proxy_id?: number | null; tag_ids?: number[] }>) { // Update parameter type to include 'type'
async updateConnection(connectionId: number, updatedData: Partial<Omit<ConnectionInfo, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { type?: 'SSH' | 'RDP'; password?: string; private_key?: string; passphrase?: string; proxy_id?: number | null; tag_ids?: number[] }>) {
this.isLoading = true; this.isLoading = true;
this.error = null; this.error = null;
try { try {