diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts index 70858694..df9ab936 100644 --- a/src/backend/database/routes/credentials.ts +++ b/src/backend/database/routes/credentials.ts @@ -1255,6 +1255,10 @@ async function deploySSHKeyToHost( return rejectAdd(err); } + stream.on("data", () => { + // Consume output + }); + stream.on("close", (code) => { clearTimeout(addTimeout); if (code === 0) { @@ -1515,7 +1519,8 @@ router.post( }); } - if (!credData.publicKey) { + const publicKey = credData.public_key || credData.publicKey; + if (!publicKey) { return res.status(400).json({ success: false, error: "Public key is required for deployment", @@ -1596,7 +1601,7 @@ router.post( const deployResult = await deploySSHKeyToHost( hostConfig, - credData.publicKey as string, + publicKey as string, credData, ); diff --git a/src/backend/database/routes/snippets.ts b/src/backend/database/routes/snippets.ts index 2147160f..521fe7af 100644 --- a/src/backend/database/routes/snippets.ts +++ b/src/backend/database/routes/snippets.ts @@ -300,13 +300,19 @@ router.post( const { Client } = await import("ssh2"); const { sshData, sshCredentials } = await import("../db/schema.js"); - // Get host configuration - const hostResult = await db - .select() - .from(sshData) - .where( - and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)), - ); + // Get host configuration using SimpleDBOps to decrypt credentials + const { SimpleDBOps } = await import("../../utils/simple-db-ops.js"); + + const hostResult = await SimpleDBOps.select( + db + .select() + .from(sshData) + .where( + and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)), + ), + "ssh_data", + userId, + ); if (hostResult.length === 0) { return res.status(404).json({ error: "Host not found" }); @@ -318,23 +324,31 @@ router.post( let password = host.password; let privateKey = host.key; let passphrase = host.key_password; + let authType = host.authType; if (host.credentialId) { - const credResult = await db - .select() - .from(sshCredentials) - .where( - and( - eq(sshCredentials.id, host.credentialId), - eq(sshCredentials.userId, userId), + const credResult = await SimpleDBOps.select( + db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, host.credentialId as number), + eq(sshCredentials.userId, userId), + ), ), - ); + "ssh_credentials", + userId, + ); if (credResult.length > 0) { const cred = credResult[0]; - password = cred.password || undefined; - privateKey = cred.private_key || cred.key || undefined; - passphrase = cred.key_password || undefined; + authType = (cred.auth_type || cred.authType || authType) as string; + password = (cred.password || undefined) as string | undefined; + privateKey = (cred.private_key || cred.key || undefined) as + | string + | undefined; + passphrase = (cred.key_password || undefined) as string | undefined; } } @@ -457,12 +471,24 @@ router.post( }, }; - if (password) { + // Set auth based on authType (like terminal.ts does) + if (authType === "password" && password) { config.password = password; - } - - if (privateKey) { - const cleanKey = privateKey + } else if (authType === "key" && privateKey) { + const cleanKey = (privateKey as string) + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + config.privateKey = Buffer.from(cleanKey, "utf8"); + if (passphrase) { + config.passphrase = passphrase; + } + } else if (password) { + // Fallback: if authType not set but password exists + config.password = password; + } else if (privateKey) { + // Fallback: if authType not set but key exists + const cleanKey = (privateKey as string) .trim() .replace(/\r\n/g, "\n") .replace(/\r/g, "\n"); diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 2db0d5b4..f1920c71 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -295,7 +295,12 @@ class SSHConnectionPool { ), ); } else if (host.password) { - const responses = prompts.map(() => host.password || ""); + const responses = prompts.map((p) => { + if (/password/i.test(p.prompt)) { + return host.password || ""; + } + return ""; + }); finish(responses); } else { finish(prompts.map(() => "")); @@ -595,7 +600,7 @@ interface SSHHostWithCredentials { defaultPath: string; tunnelConnections: unknown[]; jumpHosts?: Array<{ hostId: number }>; - statsConfig?: string; + statsConfig?: string | StatsConfig; createdAt: string; updatedAt: string; userId: string; @@ -640,19 +645,43 @@ class PollingManager { } >(); - parseStatsConfig(statsConfigStr?: string): StatsConfig { + parseStatsConfig(statsConfigStr?: string | StatsConfig): StatsConfig { if (!statsConfigStr) { return DEFAULT_STATS_CONFIG; } - try { - const parsed = JSON.parse(statsConfigStr); - return { ...DEFAULT_STATS_CONFIG, ...parsed }; - } catch (error) { - statsLogger.warn( - `Failed to parse statsConfig: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - return DEFAULT_STATS_CONFIG; + + let parsed: StatsConfig; + + // If it's already an object, use it directly + if (typeof statsConfigStr === "object") { + parsed = statsConfigStr; + } else { + // Otherwise, parse as JSON string (may be double-encoded) + try { + let temp: any = JSON.parse(statsConfigStr); + + // Check if we got a string back (double-encoded JSON) + if (typeof temp === "string") { + temp = JSON.parse(temp); + } + + parsed = temp; + } catch (error) { + statsLogger.warn( + `Failed to parse statsConfig: ${error instanceof Error ? error.message : "Unknown error"}`, + { + operation: "parse_stats_config_error", + statsConfigStr, + }, + ); + return DEFAULT_STATS_CONFIG; + } } + + // Merge with defaults, but prioritize the provided values + const result = { ...DEFAULT_STATS_CONFIG, ...parsed }; + + return result; } async startPollingForHost(host: SSHHostWithCredentials): Promise { @@ -664,9 +693,11 @@ class PollingManager { if (existingConfig) { if (existingConfig.statusTimer) { clearInterval(existingConfig.statusTimer); + existingConfig.statusTimer = undefined; } if (existingConfig.metricsTimer) { clearInterval(existingConfig.metricsTimer); + existingConfig.metricsTimer = undefined; } } @@ -675,14 +706,6 @@ class PollingManager { this.pollingConfigs.delete(host.id); this.statusStore.delete(host.id); this.metricsStore.delete(host.id); - statsLogger.info( - `Stopped all polling for host ${host.id} (${host.name || host.ip}) - both checks disabled`, - { - operation: "polling_stopped", - hostId: host.id, - hostName: host.name || host.ip, - }, - ); return; } @@ -691,23 +714,20 @@ class PollingManager { statsConfig, }; - // Set the config FIRST to prevent race conditions with old timers - this.pollingConfigs.set(host.id, config); - if (statsConfig.statusCheckEnabled) { const intervalMs = statsConfig.statusCheckInterval * 1000; this.pollHostStatus(host); config.statusTimer = setInterval(() => { - this.pollHostStatus(host); + // Always get the latest config to check if polling is still enabled + const latestConfig = this.pollingConfigs.get(host.id); + if (latestConfig && latestConfig.statsConfig.statusCheckEnabled) { + this.pollHostStatus(latestConfig.host); + } }, intervalMs); } else { this.statusStore.delete(host.id); - statsLogger.debug(`Status polling disabled for host ${host.id}`, { - operation: "status_polling_disabled", - hostId: host.id, - }); } if (statsConfig.metricsEnabled) { @@ -717,17 +737,17 @@ class PollingManager { this.pollHostMetrics(host); config.metricsTimer = setInterval(() => { - this.pollHostMetrics(host); + // Always get the latest config to check if polling is still enabled + const latestConfig = this.pollingConfigs.get(host.id); + if (latestConfig && latestConfig.statsConfig.metricsEnabled) { + this.pollHostMetrics(latestConfig.host); + } }, intervalMs); } else { this.metricsStore.delete(host.id); - statsLogger.debug(`Metrics polling disabled for host ${host.id}`, { - operation: "metrics_polling_disabled", - hostId: host.id, - }); } - // Update with the new timers + // Set the config with the new timers this.pollingConfigs.set(host.id, config); } @@ -787,9 +807,11 @@ class PollingManager { if (config) { if (config.statusTimer) { clearInterval(config.statusTimer); + config.statusTimer = undefined; } if (config.metricsTimer) { clearInterval(config.metricsTimer); + config.metricsTimer = undefined; } this.pollingConfigs.delete(hostId); if (clearData) { @@ -994,6 +1016,7 @@ async function resolveHostCredentials( tunnelConnections: host.tunnelConnections ? JSON.parse(host.tunnelConnections as string) : [], + jumpHosts: host.jumpHosts ? JSON.parse(host.jumpHosts as string) : [], statsConfig: host.statsConfig || undefined, createdAt: host.createdAt, updatedAt: host.updatedAt, diff --git a/src/ui/desktop/apps/credentials/CredentialsManager.tsx b/src/ui/desktop/apps/credentials/CredentialsManager.tsx index 7b3e02fa..0bd1efc0 100644 --- a/src/ui/desktop/apps/credentials/CredentialsManager.tsx +++ b/src/ui/desktop/apps/credentials/CredentialsManager.tsx @@ -827,13 +827,10 @@ export function CredentialsManager({ )} - +
-
- -
{t("credentials.deploySSHKey")} @@ -1009,7 +1006,7 @@ export function CredentialsManager({