import { ArrowLeft, ChevronDown, Eye, EyeOff, Globe, Key, Lock, Plus, Terminal as TerminalIcon, User, } from "lucide-react"; import React, { useMemo, useState } from "react"; import { cn } from "../lib/utils"; import { Host, KnownHost, SSHKey } from "../types"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; import { ScrollArea } from "./ui/scroll-area"; // Protocol types supported for quick connect type Protocol = "ssh" | "mosh" | "telnet"; // Wizard steps type WizardStep = "protocol" | "username" | "knownhost" | "auth"; interface QuickConnectTarget { hostname: string; username?: string; port?: number; } interface QuickConnectWizardProps { open: boolean; target: QuickConnectTarget; keys: SSHKey[]; knownHosts: KnownHost[]; onConnect: (host: Host) => void; onSaveHost?: (host: Host) => void; onAddKey?: () => void; onClose: () => void; } // Parse user@host:port format export function parseQuickConnectInput( input: string, ): QuickConnectTarget | null { const trimmed = input.trim(); if (!trimmed) return null; // Pattern: [user@]hostname[:port] // Hostname can be IP (v4 or v6) or domain name const regex = /^(?:([^@]+)@)?([^\s:]+|\[[^\]]+\])(?::(\d+))?$/; const match = trimmed.match(regex); if (!match) return null; const [, username, hostname, portStr] = match; // Validate hostname looks like an IP or domain const ipv4Regex = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; const ipv6Regex = /^\[?[a-fA-F0-9:]+\]?$/; const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/; if ( !ipv4Regex.test(hostname) && !ipv6Regex.test(hostname) && !domainRegex.test(hostname) ) { return null; } const port = portStr ? parseInt(portStr, 10) : undefined; if (port !== undefined && (isNaN(port) || port < 1 || port > 65535)) { return null; } return { hostname: hostname.replace(/^\[|\]$/g, ""), // Remove IPv6 brackets username: username || undefined, port, }; } // Check if input looks like a quick connect address export function isQuickConnectInput(input: string): boolean { return parseQuickConnectInput(input) !== null; } const QuickConnectWizard: React.FC = ({ open, target, keys, knownHosts, onConnect, onSaveHost, onAddKey, onClose, }) => { // Wizard state const [step, setStep] = useState("protocol"); const [protocol, setProtocol] = useState("ssh"); const [username, setUsername] = useState(target.username || ""); const [port, setPort] = useState(target.port || 22); const [moshServerPath, setMoshServerPath] = useState(""); const [showLogs, setShowLogs] = useState(false); // Known host verification state const [knownHostInfo, setKnownHostInfo] = useState<{ keyType: string; fingerprint: string; } | null>(null); // Auth state const [authMethod, setAuthMethod] = useState<"password" | "key">("password"); const [password, setPassword] = useState(""); const [selectedKeyId, setSelectedKeyId] = useState(null); const [showPassword, setShowPassword] = useState(false); const [saveCredentials, _setSaveCredentials] = useState(true); // Check if host is in known hosts const _existingKnownHost = useMemo(() => { return knownHosts.find( (kh) => kh.hostname === target.hostname && (kh.port === port || (!kh.port && port === 22)), ); }, [knownHosts, target.hostname, port]); // Reset state when target changes React.useEffect(() => { if (open) { setStep("protocol"); setProtocol("ssh"); setUsername(target.username || ""); setPort(target.port || 22); setPassword(""); setSelectedKeyId(null); setShowPassword(false); setKnownHostInfo(null); } }, [open, target]); // Get default port for protocol const getDefaultPort = (proto: Protocol) => { switch (proto) { case "ssh": return 22; case "mosh": return 22; case "telnet": return 23; default: return 22; } }; // Handle protocol selection const handleProtocolSelect = (proto: Protocol) => { setProtocol(proto); // Update port to default for protocol if unchanged if (port === getDefaultPort(protocol)) { setPort(getDefaultPort(proto)); } }; // Navigate to next step const handleContinue = () => { switch (step) { case "protocol": // Always go to username step to let user confirm/edit username setStep("username"); break; case "username": setStep("auth"); break; case "knownhost": setStep("auth"); break; case "auth": handleConnect(); break; } }; // Navigate back const handleBack = () => { switch (step) { case "username": setStep("protocol"); break; case "knownhost": setStep("username"); break; case "auth": // Always go back to username step setStep("username"); break; } }; // Create host and connect const handleConnect = () => { const effectiveUsername = username || target.username || "root"; const effectivePort = port || getDefaultPort(protocol); const tempHost: Host = { id: `quick-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, label: `${target.hostname}`, hostname: target.hostname, port: effectivePort, username: effectiveUsername, group: "", tags: [], os: "linux", protocol: protocol === "mosh" ? "ssh" : protocol, authMethod: authMethod, password: authMethod === "password" ? password : undefined, identityFileId: authMethod === "key" ? selectedKeyId || undefined : undefined, moshEnabled: protocol === "mosh", // Set telnet-specific fields when using telnet protocol telnetEnabled: protocol === "telnet", telnetPort: protocol === "telnet" ? effectivePort : undefined, createdAt: Date.now(), }; // Save host if requested if (saveCredentials && onSaveHost) { onSaveHost(tempHost); } onConnect(tempHost); onClose(); }; // Check if can proceed const canProceed = useMemo(() => { switch (step) { case "protocol": return true; case "username": return username.trim().length > 0; case "knownhost": return true; case "auth": if (authMethod === "password") { return password.trim().length > 0; } return !!selectedKeyId; } }, [step, username, authMethod, password, selectedKeyId]); // Render protocol selection step const renderProtocolStep = () => (

Choose protocol

{/* SSH */} {/* Mosh */} {/* Telnet */}
); // Render username step const renderUsernameStep = () => (
setUsername(e.target.value)} placeholder="root" autoFocus onKeyDown={(e) => { if (e.key === "Enter" && username.trim()) { handleContinue(); } }} />
); // Render known host verification step const renderKnownHostStep = () => (

Are you sure you want to connect?

The authenticity of {target.hostname} can not be established.

{knownHostInfo && ( <>

{knownHostInfo.keyType} fingerprint is SHA256:

{knownHostInfo.fingerprint}

)}

Do you want to add it to the list of known hosts?

); // Render auth step const renderAuthStep = () => (
{/* Auth method tabs */}
{/* Password field */} {authMethod === "password" && (
setPassword(e.target.value)} placeholder="Enter password" className="pr-10" autoFocus onKeyDown={(e) => { if (e.key === "Enter" && password.trim()) { handleConnect(); } }} />
)} {/* Key selection */} {authMethod === "key" && (
{keys.filter((k) => k.category === "key").length === 0 ? (
No keys available. Add keys in the Keychain section.
) : (
{keys .filter((k) => k.category === "key") .map((key) => ( ))}
)} {onAddKey && ( )}
)}
); // Get step title const getStepTitle = () => { switch (step) { case "protocol": return target.hostname; case "username": return target.hostname; case "knownhost": return target.hostname; case "auth": return target.hostname; } }; // Get step subtitle const getStepSubtitle = () => { const effectiveUsername = username || target.username || ""; switch (step) { case "protocol": return target.hostname; case "username": return `${protocol.toUpperCase()} ${target.hostname}:${port}`; case "knownhost": return `${protocol.toUpperCase()} ${effectiveUsername}@${target.hostname}:${port}`; case "auth": return `${protocol.toUpperCase()} ${target.hostname}:${port}`; } }; // Render progress indicator const renderProgressIndicator = () => { const steps: WizardStep[] = target.username ? ["protocol", "auth"] : ["protocol", "username", "auth"]; const currentIndex = steps.indexOf(step); return (
= 0 ? "bg-primary/20 text-primary" : "bg-muted text-muted-foreground", )} >
{!target.username && ( <>
= 1 ? "bg-primary/20 text-primary" : "bg-muted text-muted-foreground", )} >
)}
{authMethod === "password" ? : }
{">_"}
); }; return (
e.stopPropagation()} > {/* Header */}

{getStepTitle()}

{getStepSubtitle()}

{(step === "auth" || (step === "knownhost" && !knownHostInfo)) && ( )}
{/* Progress indicator */}
{renderProgressIndicator()}
{/* Content */}
{step === "protocol" && renderProtocolStep()} {step === "username" && renderUsernameStep()} {step === "knownhost" && renderKnownHostStep()} {step === "auth" && renderAuthStep()}
{/* Footer */}
{step === "knownhost" ? (
) : step === "auth" ? (
{onAddKey && authMethod === "key" && keys.filter((k) => k.category === "key").length === 0 && ( )}
) : ( )}
); }; export default QuickConnectWizard;