fix: resolve 8 Vue 3 lifecycle and component issues
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 4m5s
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 4m5s
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 <script setup> top level in DetachedSession VUE-4: call useTerminal() before nextTick await in CopilotPanel launch() VUE-5: remove duplicate ResizeObserver from LocalTerminalView (useTerminal already creates one) VUE-6: store terminal.onResize() IDisposable, dispose in onBeforeUnmount VUE-7: extract connectSsh(), connectRdp(), resolveCredentials() from 220-line connect() VUE-8: check session protocol before ssh_resize vs pty_resize in TerminalView Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1b7b1a0051
commit
ff9fc798c3
@ -203,25 +203,28 @@ async function launch(): Promise<void> {
|
|||||||
});
|
});
|
||||||
connected.value = true;
|
connected.value = true;
|
||||||
|
|
||||||
await nextTick();
|
// Instantiate terminal synchronously (before any further awaits) now that
|
||||||
|
// sessionId is known. Cleanup is owned by this component's onBeforeUnmount.
|
||||||
|
terminalInstance = useTerminal(sessionId, "pty");
|
||||||
|
|
||||||
if (containerRef.value) {
|
nextTick(() => {
|
||||||
terminalInstance = useTerminal(sessionId, "pty");
|
if (containerRef.value && terminalInstance) {
|
||||||
terminalInstance.mount(containerRef.value);
|
terminalInstance.mount(containerRef.value);
|
||||||
|
|
||||||
// Fit after mount to get real dimensions, then resize the PTY
|
// Fit after mount to get real dimensions, then resize the PTY
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (terminalInstance) {
|
if (terminalInstance) {
|
||||||
terminalInstance.fit();
|
terminalInstance.fit();
|
||||||
const term = terminalInstance.terminal;
|
const term = terminalInstance.terminal;
|
||||||
invoke("pty_resize", {
|
invoke("pty_resize", {
|
||||||
sessionId,
|
sessionId,
|
||||||
cols: term.cols,
|
cols: term.cols,
|
||||||
rows: term.rows,
|
rows: term.rows,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Listen for shell exit
|
// Listen for shell exit
|
||||||
closeUnlisten = await listen(`pty:close:${sessionId}`, () => {
|
closeUnlisten = await listen(`pty:close:${sessionId}`, () => {
|
||||||
|
|||||||
@ -26,36 +26,34 @@ const sessionName = ref("Detached Session");
|
|||||||
const protocol = ref("ssh");
|
const protocol = ref("ssh");
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
let terminalInstance: ReturnType<typeof useTerminal> | null = null;
|
// Parse session info from URL hash synchronously so backend type is known at setup time
|
||||||
|
const hash = window.location.hash;
|
||||||
|
const params = new URLSearchParams(hash.split("?")[1] || "");
|
||||||
|
const _initialSessionId = params.get("sessionId") || "";
|
||||||
|
const _initialProtocol = params.get("protocol") || "ssh";
|
||||||
|
const _backend = (_initialProtocol === "local" ? "pty" : "ssh") as 'ssh' | 'pty';
|
||||||
|
|
||||||
|
const terminalInstance = useTerminal(_initialSessionId, _backend);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Parse session info from URL hash
|
sessionId.value = _initialSessionId;
|
||||||
const hash = window.location.hash;
|
|
||||||
const params = new URLSearchParams(hash.split("?")[1] || "");
|
|
||||||
sessionId.value = params.get("sessionId") || "";
|
|
||||||
sessionName.value = decodeURIComponent(params.get("name") || "Detached Session");
|
sessionName.value = decodeURIComponent(params.get("name") || "Detached Session");
|
||||||
protocol.value = params.get("protocol") || "ssh";
|
protocol.value = _initialProtocol;
|
||||||
|
|
||||||
if (!sessionId.value || !containerRef.value) return;
|
if (!sessionId.value || !containerRef.value) return;
|
||||||
|
|
||||||
// Determine backend type
|
|
||||||
const backend = protocol.value === "local" ? "pty" : "ssh";
|
|
||||||
terminalInstance = useTerminal(sessionId.value, backend as 'ssh' | 'pty');
|
|
||||||
terminalInstance.mount(containerRef.value);
|
terminalInstance.mount(containerRef.value);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (terminalInstance) {
|
terminalInstance.fit();
|
||||||
terminalInstance.fit();
|
terminalInstance.terminal.focus();
|
||||||
terminalInstance.terminal.focus();
|
|
||||||
|
|
||||||
// Resize the backend
|
const resizeCmd = _backend === "ssh" ? "ssh_resize" : "pty_resize";
|
||||||
const resizeCmd = backend === "ssh" ? "ssh_resize" : "pty_resize";
|
invoke(resizeCmd, {
|
||||||
invoke(resizeCmd, {
|
sessionId: sessionId.value,
|
||||||
sessionId: sessionId.value,
|
cols: terminalInstance.terminal.cols,
|
||||||
cols: terminalInstance.terminal.cols,
|
rows: terminalInstance.terminal.rows,
|
||||||
rows: terminalInstance.terminal.rows,
|
}).catch(() => {});
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
// On window close, emit event so main window reattaches the tab
|
// On window close, emit event so main window reattaches the tab
|
||||||
@ -72,9 +70,6 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (terminalInstance) {
|
terminalInstance.destroy();
|
||||||
terminalInstance.destroy();
|
|
||||||
terminalInstance = null;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -21,19 +21,10 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
const { terminal, mount, fit, destroy } = useTerminal(props.sessionId, "pty");
|
const { terminal, mount, fit, destroy } = useTerminal(props.sessionId, "pty");
|
||||||
let resizeObserver: ResizeObserver | null = null;
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
mount(containerRef.value);
|
mount(containerRef.value);
|
||||||
|
|
||||||
// Watch container size changes and refit terminal
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
|
||||||
if (props.isActive) {
|
|
||||||
fit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resizeObserver.observe(containerRef.value);
|
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fit();
|
fit();
|
||||||
@ -66,10 +57,6 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (resizeObserver) {
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
resizeObserver = null;
|
|
||||||
}
|
|
||||||
destroy();
|
destroy();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -59,11 +59,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, nextTick, onMounted, watch } from "vue";
|
import { ref, nextTick, onMounted, onBeforeUnmount, watch } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useTerminal } from "@/composables/useTerminal";
|
import { useTerminal } from "@/composables/useTerminal";
|
||||||
import { useSessionStore } from "@/stores/session.store";
|
import { useSessionStore } from "@/stores/session.store";
|
||||||
import MonitorBar from "@/components/terminal/MonitorBar.vue";
|
import MonitorBar from "@/components/terminal/MonitorBar.vue";
|
||||||
|
import type { IDisposable } from "@xterm/xterm";
|
||||||
import "@/assets/css/terminal.css";
|
import "@/assets/css/terminal.css";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -74,6 +75,7 @@ const props = defineProps<{
|
|||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
const { terminal, searchAddon, mount, fit } = useTerminal(props.sessionId);
|
const { terminal, searchAddon, mount, fit } = useTerminal(props.sessionId);
|
||||||
|
let resizeDisposable: IDisposable | null = null;
|
||||||
|
|
||||||
// --- Search state ---
|
// --- Search state ---
|
||||||
const searchVisible = ref(false);
|
const searchVisible = ref(false);
|
||||||
@ -139,7 +141,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Track terminal dimensions in the session store
|
// Track terminal dimensions in the session store
|
||||||
terminal.onResize(({ cols, rows }) => {
|
resizeDisposable = terminal.onResize(({ cols, rows }) => {
|
||||||
sessionStore.setTerminalDimensions(props.sessionId, cols, rows);
|
sessionStore.setTerminalDimensions(props.sessionId, cols, rows);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -162,7 +164,9 @@ watch(
|
|||||||
fit();
|
fit();
|
||||||
terminal.focus();
|
terminal.focus();
|
||||||
// Also notify the backend of the correct size
|
// Also notify the backend of the correct size
|
||||||
invoke("ssh_resize", {
|
const session = sessionStore.sessions.find(s => s.id === props.sessionId);
|
||||||
|
const resizeCmd = session?.protocol === "local" ? "pty_resize" : "ssh_resize";
|
||||||
|
invoke(resizeCmd, {
|
||||||
sessionId: props.sessionId,
|
sessionId: props.sessionId,
|
||||||
cols: terminal.cols,
|
cols: terminal.cols,
|
||||||
rows: terminal.rows,
|
rows: terminal.rows,
|
||||||
@ -205,6 +209,13 @@ watch(() => sessionStore.activeTheme, (newTheme) => {
|
|||||||
if (newTheme) applyTheme();
|
if (newTheme) applyTheme();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (resizeDisposable) {
|
||||||
|
resizeDisposable.dispose();
|
||||||
|
resizeDisposable = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function handleFocus(): void {
|
function handleFocus(): void {
|
||||||
terminal.focus();
|
terminal.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -494,15 +494,19 @@ function handleKeydown(event: KeyboardEvent): void {
|
|||||||
if (ctrl && event.key === "f") { const active = sessionStore.activeSession; if (active?.protocol === "ssh") { event.preventDefault(); sessionContainer.value?.openActiveSearch(); } return; }
|
if (ctrl && event.key === "f") { const active = sessionStore.activeSession; if (active?.protocol === "ssh") { event.preventDefault(); sessionContainer.value?.openActiveSearch(); } return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let workspaceSaveInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
function handleBeforeUnload(e: BeforeUnloadEvent): void {
|
||||||
|
if (sessionStore.sessions.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
document.addEventListener("keydown", handleKeydown);
|
document.addEventListener("keydown", handleKeydown);
|
||||||
|
|
||||||
// Confirm before closing if sessions are active (synchronous — won't hang)
|
// Confirm before closing if sessions are active (synchronous — won't hang)
|
||||||
window.addEventListener("beforeunload", (e) => {
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||||
if (sessionStore.sessions.length > 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await connectionStore.loadAll();
|
await connectionStore.loadAll();
|
||||||
|
|
||||||
@ -520,7 +524,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// Auto-save workspace every 30 seconds instead of on close
|
// Auto-save workspace every 30 seconds instead of on close
|
||||||
// (onCloseRequested was hanging the window close on Windows)
|
// (onCloseRequested was hanging the window close on Windows)
|
||||||
setInterval(() => {
|
workspaceSaveInterval = setInterval(() => {
|
||||||
const tabs = sessionStore.sessions
|
const tabs = sessionStore.sessions
|
||||||
.filter(s => s.protocol === "ssh" || s.protocol === "rdp")
|
.filter(s => s.protocol === "ssh" || s.protocol === "rdp")
|
||||||
.map((s, i) => ({ connectionId: s.connectionId, protocol: s.protocol, position: i }));
|
.map((s, i) => ({ connectionId: s.connectionId, protocol: s.protocol, position: i }));
|
||||||
@ -543,5 +547,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener("keydown", handleKeydown);
|
document.removeEventListener("keydown", handleKeydown);
|
||||||
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||||
|
if (workspaceSaveInterval !== null) {
|
||||||
|
clearInterval(workspaceSaveInterval);
|
||||||
|
workspaceSaveInterval = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -115,14 +115,173 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
return count === 0 ? baseName : `${baseName} (${count + 1})`;
|
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<CredentialRow | null> {
|
||||||
|
try {
|
||||||
|
const allCreds = await invoke<CredentialRow[]>("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<void> {
|
||||||
|
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<string>("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<string>("decrypt_password", { credentialId: cred.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!resolvedUsername) throw new Error("NO_CREDENTIALS");
|
||||||
|
sessionId = await invoke<string>("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<string>("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<void> {
|
||||||
|
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<string>("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<string>("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<string>("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.
|
* Connect to a server by connection ID.
|
||||||
* Multiple sessions to the same host are allowed (MobaXTerm-style).
|
* Multiple sessions to the same host are allowed (MobaXTerm-style).
|
||||||
* Each gets its own tab with a disambiguated name like "Asgard (2)".
|
* 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<void> {
|
async function connect(connectionId: number): Promise<void> {
|
||||||
const connectionStore = useConnectionStore();
|
const connectionStore = useConnectionStore();
|
||||||
@ -132,214 +291,14 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
connecting.value = true;
|
connecting.value = true;
|
||||||
try {
|
try {
|
||||||
if (conn.protocol === "ssh") {
|
if (conn.protocol === "ssh") {
|
||||||
let sessionId: string;
|
await connectSsh(conn, connectionId);
|
||||||
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<string>("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<string>("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<string>("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<string>("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;
|
|
||||||
} else if (conn.protocol === "rdp") {
|
} else if (conn.protocol === "rdp") {
|
||||||
let username = "";
|
await connectRdp(conn, connectionId);
|
||||||
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<string>("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<string>("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<string>("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;
|
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
||||||
console.error("Connection failed:", msg);
|
console.error("Connection failed:", msg);
|
||||||
lastError.value = msg;
|
lastError.value = msg;
|
||||||
// Show error as native alert so it's visible without DevTools
|
|
||||||
alert(`Connection failed: ${msg}`);
|
alert(`Connection failed: ${msg}`);
|
||||||
} finally {
|
} finally {
|
||||||
connecting.value = false;
|
connecting.value = false;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user