diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index cef1130c..e912cdc9 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -678,7 +678,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { decrypted.authType, decrypted.password || null, decrypted.key || null, - decrypted.keyPassword || decrypted.key_password || null, + decrypted.key_password || null, decrypted.keyType || null, decrypted.autostartPassword || null, decrypted.autostartKey || null, @@ -721,9 +721,9 @@ app.post("/database/export", authenticateJWT, async (req, res) => { decrypted.username, decrypted.password || null, decrypted.key || null, - decrypted.privateKey || decrypted.private_key || null, - decrypted.publicKey || decrypted.public_key || null, - decrypted.keyPassword || decrypted.key_password || null, + decrypted.private_key || null, + decrypted.public_key || null, + decrypted.key_password || null, decrypted.keyType || null, decrypted.detectedKeyType || null, decrypted.usageCount || 0, @@ -1006,7 +1006,6 @@ app.post( }; try { - try { const importedHosts = importDb .prepare("SELECT * FROM ssh_data") diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 66481401..0f381440 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -338,6 +338,38 @@ wss.on("connection", async (ws: WebSocket, req) => { break; } + case "password_response": { + const passwordData = data as TOTPResponseData; // Same structure + if (keyboardInteractiveFinish && passwordData?.code) { + const password = passwordData.code; + sshLogger.info("Password received from user", { + operation: "password_response", + userId, + passwordLength: password.length, + }); + + keyboardInteractiveFinish([password]); + keyboardInteractiveFinish = null; + } else { + sshLogger.warn( + "Password response received but no callback available", + { + operation: "password_response_error", + userId, + hasCallback: !!keyboardInteractiveFinish, + hasCode: !!passwordData?.code, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: "Password authentication state lost. Please reconnect.", + }), + ); + } + break; + } + default: sshLogger.warn("Unknown message type received", { operation: "websocket_message_unknown_type", @@ -779,7 +811,7 @@ wss.on("connection", async (ws: WebSocket, req) => { }), ); } else { - // Non-TOTP prompts (password, etc.) - respond automatically + // Non-TOTP prompts (password, etc.) if (keyboardInteractiveResponded) { sshLogger.warn( "Already responded to keyboard-interactive, ignoring subsequent prompt", @@ -793,6 +825,54 @@ wss.on("connection", async (ws: WebSocket, req) => { } keyboardInteractiveResponded = true; + // Check if we have stored credentials for auto-response + const hasStoredPassword = + resolvedCredentials.password && + resolvedCredentials.authType !== "none"; + + if (!hasStoredPassword && resolvedCredentials.authType === "none") { + // For "none" auth type, prompt user for all keyboard-interactive inputs + const passwordPromptIndex = prompts.findIndex((p) => + /password/i.test(p.prompt), + ); + + if (passwordPromptIndex !== -1) { + // Ask user for password + keyboardInteractiveFinish = (userResponses: string[]) => { + const userInput = (userResponses[0] || "").trim(); + + // Build responses for all prompts + const responses = prompts.map((p, index) => { + if (index === passwordPromptIndex) { + return userInput; + } + return ""; + }); + + sshLogger.info( + "User-provided password being sent to SSH server", + { + operation: "interactive_password_verification", + hostId: id, + passwordLength: userInput.length, + totalPrompts: prompts.length, + }, + ); + + finish(responses); + }; + + ws.send( + JSON.stringify({ + type: "password_required", + prompt: prompts[passwordPromptIndex].prompt, + }), + ); + return; + } + } + + // Auto-respond with stored credentials const responses = prompts.map((p) => { if (/password/i.test(p.prompt) && resolvedCredentials.password) { return resolvedCredentials.password; @@ -882,7 +962,23 @@ wss.on("connection", async (ws: WebSocket, req) => { }, }; - if (resolvedCredentials.authType === "key" && resolvedCredentials.key) { + if (resolvedCredentials.authType === "none") { + // No credentials provided - rely entirely on keyboard-interactive authentication + // This mimics the behavior of the ssh command-line client where it prompts for password/TOTP + sshLogger.info( + "Using interactive authentication (no stored credentials)", + { + operation: "ssh_auth_none", + hostId: id, + ip, + username, + }, + ); + // Don't set password or privateKey - let keyboard-interactive handle everything + } else if ( + resolvedCredentials.authType === "key" && + resolvedCredentials.key + ) { try { if ( !resolvedCredentials.key.includes("-----BEGIN") || diff --git a/src/types/index.ts b/src/types/index.ts index f4748628..a5787bc6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -18,7 +18,7 @@ export interface SSHHost { folder: string; tags: string[]; pin: boolean; - authType: "password" | "key" | "credential"; + authType: "password" | "key" | "credential" | "none"; password?: string; key?: string; keyPassword?: string; @@ -48,7 +48,7 @@ export interface SSHHostData { folder?: string; tags?: string[]; pin?: boolean; - authType: "password" | "key" | "credential"; + authType: "password" | "key" | "credential" | "none"; password?: string; key?: File | null; keyPassword?: string; @@ -298,7 +298,7 @@ export type ErrorType = // AUTHENTICATION TYPES // ============================================================================ -export type AuthType = "password" | "key" | "credential"; +export type AuthType = "password" | "key" | "credential" | "none"; export type KeyType = "rsa" | "ecdsa" | "ed25519"; diff --git a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx index df38275f..95edeed5 100644 --- a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx +++ b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx @@ -90,9 +90,9 @@ export function HostManagerEditor({ Array<{ id: number; username: string; authType: string }> >([]); - const [authTab, setAuthTab] = useState<"password" | "key" | "credential">( - "password", - ); + const [authTab, setAuthTab] = useState< + "password" | "key" | "credential" | "none" + >("password"); const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">( "upload", ); @@ -179,7 +179,7 @@ export function HostManagerEditor({ folder: z.string().optional(), tags: z.array(z.string().min(1)).default([]), pin: z.boolean().default(false), - authType: z.enum(["password", "key", "credential"]), + authType: z.enum(["password", "key", "credential", "none"]), credentialId: z.number().optional().nullable(), password: z.string().optional(), key: z.any().optional().nullable(), @@ -241,6 +241,11 @@ export function HostManagerEditor({ }), }) .superRefine((data, ctx) => { + if (data.authType === "none") { + // No credentials required for "none" auth type - will use keyboard-interactive + return; + } + if (data.authType === "password") { if ( !data.password || @@ -356,7 +361,9 @@ export function HostManagerEditor({ ? "credential" : cleanedHost.key ? "key" - : "password"; + : cleanedHost.password + ? "password" + : "none"; setAuthTab(defaultAuthType); const formData = { @@ -367,7 +374,7 @@ export function HostManagerEditor({ folder: cleanedHost.folder || "", tags: cleanedHost.tags || [], pin: Boolean(cleanedHost.pin), - authType: defaultAuthType as "password" | "key" | "credential", + authType: defaultAuthType as "password" | "key" | "credential" | "none", credentialId: null, password: "", key: null, @@ -922,7 +929,8 @@ export function HostManagerEditor({ const newAuthType = value as | "password" | "key" - | "credential"; + | "credential" + | "none"; setAuthTab(newAuthType); form.setValue("authType", newAuthType); }} @@ -936,6 +944,7 @@ export function HostManagerEditor({ {t("hosts.credential")} + {t("hosts.none")} + + + + {t("hosts.noneAuthTitle")} +
+ {t("hosts.noneAuthDescription")} +
+
+ {t("hosts.noneAuthDetails")} +
+
+
+
diff --git a/src/ui/Desktop/Apps/Terminal/Terminal.tsx b/src/ui/Desktop/Apps/Terminal/Terminal.tsx index 9003f93c..ba276730 100644 --- a/src/ui/Desktop/Apps/Terminal/Terminal.tsx +++ b/src/ui/Desktop/Apps/Terminal/Terminal.tsx @@ -613,7 +613,6 @@ export const Terminal = forwardRef( fontSize: 14, fontFamily: '"Caskaydia Cove Nerd Font Mono", "SF Mono", Consolas, "Liberation Mono", monospace', - theme: { background: "#18181b", foreground: "#f7f7f7" }, allowTransparency: true, convertEol: true, windowsMode: false, @@ -626,6 +625,8 @@ export const Terminal = forwardRef( minimumContrastRatio: 1, letterSpacing: 0, lineHeight: 1.2, + + theme: { background: "#18181b", foreground: "#f7f7f7" }, }; const fitAddon = new FitAddon(); diff --git a/src/ui/Desktop/Navigation/Hosts/Host.tsx b/src/ui/Desktop/Navigation/Hosts/Host.tsx index bfef6df5..23be759d 100644 --- a/src/ui/Desktop/Navigation/Hosts/Host.tsx +++ b/src/ui/Desktop/Navigation/Hosts/Host.tsx @@ -104,6 +104,7 @@ export function Host({ host }: HostProps): React.ReactElement { className="min-w-[160px]" > addTab({ type: "server", title, hostConfig: host }) } @@ -111,13 +112,17 @@ export function Host({ host }: HostProps): React.ReactElement { Open Server Details addTab({ type: "file_manager", title, hostConfig: host }) } > Open File Manager - alert("Settings clicked")}> + alert("Settings clicked")} + > Edit diff --git a/src/ui/Desktop/Navigation/TopNavbar.tsx b/src/ui/Desktop/Navigation/TopNavbar.tsx index ae418620..1cd4b0fd 100644 --- a/src/ui/Desktop/Navigation/TopNavbar.tsx +++ b/src/ui/Desktop/Navigation/TopNavbar.tsx @@ -58,12 +58,15 @@ export function TopNavbar({ const [isRecording, setIsRecording] = useState(false); const [selectedTabIds, setSelectedTabIds] = useState([]); const [snippetsSidebarOpen, setSnippetsSidebarOpen] = useState(false); + const [justDroppedTabId, setJustDroppedTabId] = useState(null); // New state variable const [dragState, setDragState] = useState<{ + draggedId: number | null; draggedIndex: number | null; currentX: number; startX: number; targetIndex: number | null; }>({ + draggedId: null, draggedIndex: null, currentX: 0, startX: 0, @@ -72,6 +75,8 @@ export function TopNavbar({ const containerRef = React.useRef(null); const tabRefs = React.useRef>(new Map()); + const prevTabsRef = React.useRef([]); + const handleTabActivate = (tabId: number) => { setCurrentTab(tabId); }; @@ -249,6 +254,37 @@ export function TopNavbar({ } }; + React.useEffect(() => { + if (prevTabsRef.current.length > 0 && tabs !== prevTabsRef.current) { + // Check if tabs actually changed + console.log("Tabs AFTER reorder (IDs and references):"); + tabs.forEach((newTab, newIdx) => { + const oldTab = prevTabsRef.current.find((t) => t.id === newTab.id); + console.log( + ` [${newIdx}] ID: ${newTab.id}, Ref:`, + newTab, + `(Old Ref:`, + oldTab, + `)`, + ); + if (oldTab && oldTab !== newTab) { + console.warn(` Tab ID ${newTab.id} object reference CHANGED!`); + } else if (oldTab && oldTab === newTab) { + console.info(` Tab ID ${newTab.id} object reference PRESERVED.`); + } + }); + // Clear prevTabsRef.current only after the comparison is done + prevTabsRef.current = []; + } + }, [tabs]); // Depend only on tabs + + React.useEffect(() => { + if (justDroppedTabId !== null) { + const timer = setTimeout(() => setJustDroppedTabId(null), 50); // Clear after a short delay + return () => clearTimeout(timer); + } + }, [justDroppedTabId]); + const handleDragStart = (e: React.DragEvent, index: number) => { console.log("Drag start:", index, e.clientX); @@ -259,6 +295,7 @@ export function TopNavbar({ e.dataTransfer.setDragImage(img, 0, 0); setDragState({ + draggedId: tabs[index].id, draggedIndex: index, startX: e.clientX, currentX: e.clientX, @@ -333,11 +370,25 @@ export function TopNavbar({ // Moving right - find the rightmost tab whose midpoint we've passed for (let i = draggedIndex + 1; i < tabBoundaries.length; i++) { if (draggedCenter > tabBoundaries[i].mid) { - newTargetIndex = i; + newTargetIndex = i; // Reverted from i + 1 to i } else { break; } } + // Edge case: if dragged past the last tab, target should be at the very end + const lastTabIndex = tabBoundaries.length - 1; + if (lastTabIndex >= 0) { + // Ensure there's at least one tab + const lastTabEl = tabRefs.current.get(lastTabIndex); + if (lastTabEl) { + const lastTabRect = lastTabEl.getBoundingClientRect(); + const containerRect = containerRef.current.getBoundingClientRect(); + const lastTabEndInContainer = lastTabRect.right - containerRect.left; + if (currentX > lastTabEndInContainer) { + newTargetIndex = tabBoundaries.length; // Insert at the very end + } + } + } } return newTargetIndex; @@ -378,57 +429,40 @@ export function TopNavbar({ dragState.targetIndex !== null && dragState.draggedIndex !== dragState.targetIndex ) { + console.log("Tabs before reorder (IDs and references):"); + tabs.forEach((tab, idx) => + console.log(` [${idx}] ID: ${tab.id}, Ref:`, tab), + ); + prevTabsRef.current = tabs; // Store current tabs before reorder reorderTabs(dragState.draggedIndex, dragState.targetIndex); - - // Delay clearing drag state to prevent visual jitter - // This allows the reorder to complete and re-render before removing transforms - setTimeout(() => { - setDragState({ - draggedIndex: null, - startX: 0, - currentX: 0, - targetIndex: null, - }); - }, 0); - } else { - // No reorder needed, clear immediately - setDragState({ - draggedIndex: null, - startX: 0, - currentX: 0, - targetIndex: null, - }); + if (dragState.draggedId !== null) { + setJustDroppedTabId(dragState.draggedId); + } } + // Immediately reset drag state after drop to ensure a single re-render + // with updated tabs and cleared drag state. + setDragState({ + draggedId: null, + draggedIndex: null, + startX: 0, + currentX: 0, + targetIndex: null, + }); }; const handleDragEnd = () => { console.log("Drag end:", dragState); - if ( - dragState.draggedIndex !== null && - dragState.targetIndex !== null && - dragState.draggedIndex !== dragState.targetIndex - ) { - reorderTabs(dragState.draggedIndex, dragState.targetIndex); - - // Delay clearing drag state to prevent visual jitter - setTimeout(() => { - setDragState({ - draggedIndex: null, - startX: 0, - currentX: 0, - targetIndex: null, - }); - }, 0); - } else { - // No reorder needed, clear immediately - setDragState({ - draggedIndex: null, - startX: 0, - currentX: 0, - targetIndex: null, - }); - } + // Immediately reset drag state. If a drop occurred, handleDrop has already + // initiated the state clear. If the drag was cancelled (e.g., dropped + // outside a valid target), this clears the drag state. + setDragState({ + draggedId: null, + draggedIndex: null, + startX: 0, + currentX: 0, + targetIndex: null, + }); }; const isSplitScreenActive = @@ -492,47 +526,64 @@ export function TopNavbar({ isSplitScreenActive); const disableClose = (isSplitScreenActive && isActive) || isSplit; - const isDragging = dragState.draggedIndex === index; - const dragOffset = isDragging + const isDraggingThisTab = dragState.draggedIndex === index; + const isTheDraggedTab = tab.id === dragState.draggedId; + const isDroppedAndSnapping = tab.id === justDroppedTabId; // New condition + const dragOffset = isDraggingThisTab ? dragState.currentX - dragState.startX : 0; + // Diagnostic logs + if (dragState.draggedIndex !== null) { + console.log( + `Tab ID: ${tab.id}, Index: ${index}, isDraggingThisTab: ${isDraggingThisTab}, draggedOriginalIndex: ${dragState.draggedIndex}, currentTargetIndex: ${dragState.targetIndex}, isDroppedAndSnapping: ${isDroppedAndSnapping}`, + ); + } + // Calculate transform let transform = ""; - if (isDragging) { - // Dragged tab follows cursor + if (isDraggingThisTab) { transform = `translateX(${dragOffset}px)`; } else if ( dragState.draggedIndex !== null && dragState.targetIndex !== null ) { - // Other tabs shift to make room - const draggedIndex = dragState.draggedIndex; - const targetIndex = dragState.targetIndex; + const draggedOriginalIndex = dragState.draggedIndex; + const currentTargetIndex = dragState.targetIndex; + // Determine if this tab should shift left or right if ( - draggedIndex < targetIndex && - index > draggedIndex && - index <= targetIndex + draggedOriginalIndex < currentTargetIndex && // Dragging rightwards + index > draggedOriginalIndex && // This tab is to the right of the original position + index <= currentTargetIndex // This tab is at or before the target position ) { - // Shifting left - const draggedTabEl = tabRefs.current.get(draggedIndex); - const draggedWidth = - draggedTabEl?.getBoundingClientRect().width || 0; - transform = `translateX(-${draggedWidth + 4}px)`; + // Shift left to make space + const draggedTabWidth = + tabRefs.current + .get(draggedOriginalIndex) + ?.getBoundingClientRect().width || 0; + const gap = 4; + transform = `translateX(-${draggedTabWidth + gap}px)`; } else if ( - draggedIndex > targetIndex && - index >= targetIndex && - index < draggedIndex + draggedOriginalIndex > currentTargetIndex && // Dragging leftwards + index >= currentTargetIndex && // This tab is at or after the target position + index < draggedOriginalIndex // This tab is to the left of the original position ) { - // Shifting right - const draggedTabEl = tabRefs.current.get(draggedIndex); - const draggedWidth = - draggedTabEl?.getBoundingClientRect().width || 0; - transform = `translateX(${draggedWidth + 4}px)`; + // Shift right to make space + const draggedTabWidth = + tabRefs.current + .get(draggedOriginalIndex) + ?.getBoundingClientRect().width || 0; + const gap = 4; + transform = `translateX(${draggedTabWidth + gap}px)`; } } + // Diagnostic log for transform + if (dragState.draggedIndex !== null) { + console.log(` Tab ID: ${tab.id}, Transform: ${transform}`); + } + return (