From ff9fc798c3fff68b9c3ebf63485ca04dae9907be Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sun, 29 Mar 2026 16:38:00 -0400 Subject: [PATCH] fix: resolve 8 Vue 3 lifecycle and component issues VUE-1: store workspace save interval ID, clear in onUnmounted VUE-2: extract beforeunload handler to named function, remove in onUnmounted VUE-3: move useTerminal() to diff --git a/src/components/terminal/LocalTerminalView.vue b/src/components/terminal/LocalTerminalView.vue index 79be014..49bda8d 100644 --- a/src/components/terminal/LocalTerminalView.vue +++ b/src/components/terminal/LocalTerminalView.vue @@ -21,19 +21,10 @@ const props = defineProps<{ const containerRef = ref(null); const { terminal, mount, fit, destroy } = useTerminal(props.sessionId, "pty"); -let resizeObserver: ResizeObserver | null = null; onMounted(() => { if (containerRef.value) { mount(containerRef.value); - - // Watch container size changes and refit terminal - resizeObserver = new ResizeObserver(() => { - if (props.isActive) { - fit(); - } - }); - resizeObserver.observe(containerRef.value); } setTimeout(() => { fit(); @@ -66,10 +57,6 @@ watch( ); onBeforeUnmount(() => { - if (resizeObserver) { - resizeObserver.disconnect(); - resizeObserver = null; - } destroy(); }); diff --git a/src/components/terminal/TerminalView.vue b/src/components/terminal/TerminalView.vue index d9d1dc1..70c4df1 100644 --- a/src/components/terminal/TerminalView.vue +++ b/src/components/terminal/TerminalView.vue @@ -59,11 +59,12 @@ diff --git a/src/stores/session.store.ts b/src/stores/session.store.ts index d31b812..8b7c10c 100644 --- a/src/stores/session.store.ts +++ b/src/stores/session.store.ts @@ -115,14 +115,173 @@ export const useSessionStore = defineStore("session", () => { return count === 0 ? baseName : `${baseName} (${count + 1})`; } + type CredentialRow = { id: number; name: string; username: string | null; domain?: string | null; credentialType: string; sshKeyId: number | null }; + + async function resolveCredentials(credentialId: number): Promise { + try { + const allCreds = await invoke("list_credentials"); + return allCreds.find((c) => c.id === credentialId) ?? null; + } catch (credErr) { + console.warn("Failed to resolve credential:", credErr); + return null; + } + } + + async function connectSsh( + conn: { id: number; name: string; hostname: string; port: number; credentialId?: number | null }, + connectionId: number, + ): Promise { + let sessionId: string; + let resolvedUsername = ""; + let resolvedPassword = ""; + + if (conn.credentialId) { + const cred = await resolveCredentials(conn.credentialId); + if (cred) { + resolvedUsername = cred.username ?? ""; + if (cred.credentialType === "ssh_key" && cred.sshKeyId) { + const [privateKey, passphrase] = await invoke<[string, string]>("decrypt_ssh_key", { sshKeyId: cred.sshKeyId }); + sessionId = await invoke("connect_ssh_with_key", { + hostname: conn.hostname, + port: conn.port, + username: resolvedUsername, + privateKeyPem: privateKey, + passphrase: passphrase || null, + cols: 120, + rows: 40, + }); + sessions.value.push({ + id: sessionId, + connectionId, + name: disambiguatedName(conn.name, connectionId), + protocol: "ssh", + active: true, + username: resolvedUsername, + status: "connected", + hasActivity: false, + }); + setupStatusListeners(sessionId); + activeSessionId.value = sessionId; + return; + } else { + resolvedPassword = await invoke("decrypt_password", { credentialId: cred.id }); + } + } + } + + try { + if (!resolvedUsername) throw new Error("NO_CREDENTIALS"); + sessionId = await invoke("connect_ssh", { + hostname: conn.hostname, + port: conn.port, + username: resolvedUsername, + password: resolvedPassword, + cols: 120, + rows: 40, + }); + } catch (sshErr: unknown) { + const errMsg = sshErr instanceof Error ? sshErr.message : typeof sshErr === "string" ? sshErr : String(sshErr); + const errLower = errMsg.toLowerCase(); + if (errLower.includes("no_credentials") || errLower.includes("unable to authenticate") || errLower.includes("authentication") || errLower.includes("rejected")) { + const username = window.prompt(`Username for ${conn.hostname}:`, resolvedUsername || "root"); + if (!username) throw new Error("Connection cancelled"); + const password = window.prompt(`Password for ${username}@${conn.hostname}:`); + if (password === null) throw new Error("Connection cancelled"); + resolvedUsername = username; + sessionId = await invoke("connect_ssh", { + hostname: conn.hostname, + port: conn.port, + username, + password, + cols: 120, + rows: 40, + }); + } else { + throw sshErr; + } + } + + sessions.value.push({ + id: sessionId, + connectionId, + name: disambiguatedName(conn.name, connectionId), + protocol: "ssh", + active: true, + username: resolvedUsername, + status: "connected", + hasActivity: false, + }); + setupStatusListeners(sessionId); + activeSessionId.value = sessionId; + } + + async function connectRdp( + conn: { id: number; name: string; hostname: string; port: number; credentialId?: number | null; options?: string }, + connectionId: number, + ): Promise { + let username = ""; + let password = ""; + let domain = ""; + + if (conn.credentialId) { + const cred = await resolveCredentials(conn.credentialId); + if (cred && cred.credentialType === "password") { + username = cred.username ?? ""; + domain = cred.domain ?? ""; + password = await invoke("decrypt_password", { credentialId: cred.id }); + } + } + + if (!username && conn.options) { + try { + const opts = JSON.parse(conn.options); + if (opts?.username) username = opts.username; + if (opts?.password) password = opts.password; + if (opts?.domain) domain = opts.domain; + } catch { + // ignore malformed options + } + } + + let sessionId: string; + try { + sessionId = await invoke("connect_rdp", { + config: { hostname: conn.hostname, port: conn.port, username, password, domain, width: 1920, height: 1080 }, + }); + } catch (rdpErr: unknown) { + const errMsg = rdpErr instanceof Error ? rdpErr.message : typeof rdpErr === "string" ? rdpErr : String(rdpErr); + if (errMsg.includes("NO_CREDENTIALS") || errMsg.includes("authentication") || errMsg.includes("logon failure")) { + const promptedUsername = prompt(`Username for ${conn.hostname}:`, "Administrator"); + if (!promptedUsername) throw new Error("Connection cancelled"); + const promptedPassword = prompt(`Password for ${promptedUsername}@${conn.hostname}:`); + if (promptedPassword === null) throw new Error("Connection cancelled"); + const promptedDomain = prompt(`Domain (leave blank if none):`, "") ?? ""; + username = promptedUsername; + sessionId = await invoke("connect_rdp", { + config: { hostname: conn.hostname, port: conn.port, username: promptedUsername, password: promptedPassword, domain: promptedDomain, width: 1920, height: 1080 }, + }); + } else { + throw rdpErr; + } + } + + sessions.value.push({ + id: sessionId, + connectionId, + name: disambiguatedName(conn.name, connectionId), + protocol: "rdp", + active: true, + username, + status: "connected", + hasActivity: false, + }); + activeSessionId.value = sessionId; + } + /** * Connect to a server by connection ID. * Multiple sessions to the same host are allowed (MobaXTerm-style). * Each gets its own tab with a disambiguated name like "Asgard (2)". - * - * For Tauri: we must resolve the connection details ourselves and pass - * hostname/port/username/password directly to connect_ssh, because the - * Rust side has no knowledge of connection IDs — the vault owns credentials. */ async function connect(connectionId: number): Promise { const connectionStore = useConnectionStore(); @@ -132,214 +291,14 @@ export const useSessionStore = defineStore("session", () => { connecting.value = true; try { if (conn.protocol === "ssh") { - let sessionId: string; - let resolvedUsername = ""; - let resolvedPassword = ""; - - // If connection has a linked credential, decrypt it from the vault - if (conn.credentialId) { - try { - const allCreds = await invoke<{ id: number; name: string; username: string | null; credentialType: string; sshKeyId: number | null }[]>("list_credentials"); - const cred = allCreds.find((c) => c.id === conn.credentialId); - - if (cred) { - resolvedUsername = cred.username ?? ""; - - if (cred.credentialType === "ssh_key" && cred.sshKeyId) { - // SSH key auth — decrypt key from vault - const [privateKey, passphrase] = await invoke<[string, string]>("decrypt_ssh_key", { sshKeyId: cred.sshKeyId }); - sessionId = await invoke("connect_ssh_with_key", { - hostname: conn.hostname, - port: conn.port, - username: resolvedUsername, - privateKeyPem: privateKey, - passphrase: passphrase || null, - cols: 120, - rows: 40, - }); - - sessions.value.push({ - id: sessionId, - connectionId, - name: disambiguatedName(conn.name, connectionId), - protocol: "ssh", - active: true, - username: resolvedUsername, - status: "connected", - hasActivity: false, - }); - setupStatusListeners(sessionId); - activeSessionId.value = sessionId; - return; // early return — key auth handled - } else { - // Password auth — decrypt password from vault - resolvedPassword = await invoke("decrypt_password", { credentialId: cred.id }); - } - } - } catch (credErr) { - console.warn("Failed to resolve credential, will prompt:", credErr); - } - } - - try { - if (!resolvedUsername) { - // No credential linked — prompt immediately - throw new Error("NO_CREDENTIALS"); - } - sessionId = await invoke("connect_ssh", { - hostname: conn.hostname, - port: conn.port, - username: resolvedUsername, - password: resolvedPassword, - cols: 120, - rows: 40, - }); - } catch (sshErr: unknown) { - const errMsg = sshErr instanceof Error - ? sshErr.message - : typeof sshErr === "string" - ? sshErr - : String(sshErr); - - // If no credentials or auth failed, prompt for username/password - const errLower = errMsg.toLowerCase(); - if (errLower.includes("no_credentials") || errLower.includes("unable to authenticate") || errLower.includes("authentication") || errLower.includes("rejected")) { - const username = window.prompt(`Username for ${conn.hostname}:`, resolvedUsername || "root"); - if (!username) throw new Error("Connection cancelled"); - const password = window.prompt(`Password for ${username}@${conn.hostname}:`); - if (password === null) throw new Error("Connection cancelled"); - - resolvedUsername = username; - sessionId = await invoke("connect_ssh", { - hostname: conn.hostname, - port: conn.port, - username, - password, - cols: 120, - rows: 40, - }); - } else { - throw sshErr; - } - } - - sessions.value.push({ - id: sessionId, - connectionId, - name: disambiguatedName(conn.name, connectionId), - protocol: "ssh", - active: true, - username: resolvedUsername, - status: "connected", - hasActivity: false, - }); - setupStatusListeners(sessionId); - activeSessionId.value = sessionId; + await connectSsh(conn, connectionId); } else if (conn.protocol === "rdp") { - let username = ""; - let password = ""; - let domain = ""; - - // Try vault credentials first (same as SSH path) - if (conn.credentialId) { - try { - const allCreds = await invoke<{ id: number; name: string; username: string | null; domain: string | null; credentialType: string; sshKeyId: number | null }[]>("list_credentials"); - const cred = allCreds.find((c) => c.id === conn.credentialId); - if (cred && cred.credentialType === "password") { - username = cred.username ?? ""; - domain = cred.domain ?? ""; - password = await invoke("decrypt_password", { credentialId: cred.id }); - } - } catch (credErr) { - console.warn("Failed to resolve RDP credential from vault:", credErr); - } - } - - // Fall back to connection options JSON if vault didn't provide creds - if (!username && conn.options) { - try { - const opts = JSON.parse(conn.options); - if (opts?.username) username = opts.username; - if (opts?.password) password = opts.password; - if (opts?.domain) domain = opts.domain; - } catch { - // ignore malformed options - } - } - - let sessionId: string; - try { - sessionId = await invoke("connect_rdp", { - config: { - hostname: conn.hostname, - port: conn.port, - username, - password, - domain, - width: 1920, - height: 1080, - }, - }); - } catch (rdpErr: unknown) { - const errMsg = - rdpErr instanceof Error - ? rdpErr.message - : typeof rdpErr === "string" - ? rdpErr - : String(rdpErr); - - // If credentials are missing or rejected, prompt the operator - if ( - errMsg.includes("NO_CREDENTIALS") || - errMsg.includes("authentication") || - errMsg.includes("logon failure") - ) { - const promptedUsername = prompt( - `Username for ${conn.hostname}:`, - "Administrator", - ); - if (!promptedUsername) throw new Error("Connection cancelled"); - const promptedPassword = prompt( - `Password for ${promptedUsername}@${conn.hostname}:`, - ); - if (promptedPassword === null) throw new Error("Connection cancelled"); - const promptedDomain = prompt(`Domain (leave blank if none):`, "") ?? ""; - - username = promptedUsername; - - sessionId = await invoke("connect_rdp", { - config: { - hostname: conn.hostname, - port: conn.port, - username: promptedUsername, - password: promptedPassword, - domain: promptedDomain, - width: 1920, - height: 1080, - }, - }); - } else { - throw rdpErr; - } - } - - sessions.value.push({ - id: sessionId, - connectionId, - name: disambiguatedName(conn.name, connectionId), - protocol: "rdp", - active: true, - username, - status: "connected", - hasActivity: false, - }); - activeSessionId.value = sessionId; + await connectRdp(conn, connectionId); } } catch (err: unknown) { const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err); console.error("Connection failed:", msg); lastError.value = msg; - // Show error as native alert so it's visible without DevTools alert(`Connection failed: ${msg}`); } finally { connecting.value = false;