feat: Add support for RDP and VNC connections in SSH host management

- Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures.
- Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert.
- Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options.
- Implemented token retrieval for RDP/VNC connections using Guacamole API.
- Updated UI components to reflect connection type changes and provide appropriate connection buttons.
- Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor.
- Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions.
This commit is contained in:
starhound
2025-12-19 16:08:27 -05:00
parent 3ac7ad0bd7
commit 776f581377
12 changed files with 540 additions and 353 deletions

View File

@@ -435,6 +435,7 @@ export function HostManagerEditor({
const formSchema = z
.object({
connectionType: z.enum(["ssh", "rdp", "vnc", "telnet"]).default("ssh"),
name: z.string().optional(),
ip: z.string().min(1),
port: z.coerce.number().min(1).max(65535),
@@ -443,6 +444,10 @@ export function HostManagerEditor({
tags: z.array(z.string().min(1)).default([]),
pin: z.boolean().default(false),
authType: z.enum(["password", "key", "credential", "none"]),
// RDP/VNC specific fields
domain: z.string().optional(),
security: z.string().optional(),
ignoreCert: z.boolean().default(false),
credentialId: z.number().optional().nullable(),
overrideCredentialUsername: z.boolean().optional(),
password: z.string().optional(),
@@ -648,6 +653,7 @@ export function HostManagerEditor({
resolver: zodResolver(formSchema) as any,
defaultValues: {
name: "",
connectionType: "ssh" as const,
ip: "",
port: 22,
username: "",
@@ -682,6 +688,10 @@ export function HostManagerEditor({
tlsCert: "",
tlsKey: "",
},
// RDP/VNC specific defaults
domain: "",
security: "",
ignoreCert: false,
},
});
@@ -759,6 +769,7 @@ export function HostManagerEditor({
}
const formData = {
connectionType: (cleanedHost.connectionType || "ssh") as "ssh" | "rdp" | "vnc" | "telnet",
name: cleanedHost.name || "",
ip: cleanedHost.ip || "",
port: cleanedHost.port || 22,
@@ -801,6 +812,10 @@ export function HostManagerEditor({
forceKeyboardInteractive: Boolean(cleanedHost.forceKeyboardInteractive),
enableDocker: Boolean(cleanedHost.enableDocker),
dockerConfig: parsedDockerConfig,
// RDP/VNC specific fields
domain: cleanedHost.domain || "",
security: cleanedHost.security || "",
ignoreCert: Boolean(cleanedHost.ignoreCert),
};
if (defaultAuthType === "password") {
@@ -828,6 +843,7 @@ export function HostManagerEditor({
} else {
setAuthTab("password");
const defaultFormData = {
connectionType: "ssh" as const,
name: "",
ip: "",
port: 22,
@@ -863,6 +879,10 @@ export function HostManagerEditor({
tlsCert: "",
tlsKey: "",
},
// RDP/VNC specific defaults
domain: "",
security: "",
ignoreCert: false,
};
form.reset(defaultFormData);
@@ -910,6 +930,7 @@ export function HostManagerEditor({
}
const submitData: Record<string, unknown> = {
connectionType: data.connectionType || "ssh",
name: data.name,
ip: data.ip,
port: data.port,
@@ -931,6 +952,10 @@ export function HostManagerEditor({
statsConfig: data.statsConfig || DEFAULT_STATS_CONFIG,
terminalConfig: data.terminalConfig || DEFAULT_TERMINAL_CONFIG,
forceKeyboardInteractive: Boolean(data.forceKeyboardInteractive),
// RDP/VNC specific fields
domain: data.domain || null,
security: data.security || null,
ignoreCert: Boolean(data.ignoreCert),
};
submitData.credentialId = null;
@@ -1230,23 +1255,69 @@ export function HostManagerEditor({
onValueChange={setActiveTab}
className="w-full"
>
<TabsList>
<TabsTrigger value="general">
{t("hosts.general")}
</TabsTrigger>
<TabsTrigger value="terminal">
{t("hosts.terminal")}
</TabsTrigger>
<TabsTrigger value="docker">Docker</TabsTrigger>
<TabsTrigger value="tunnel">{t("hosts.tunnel")}</TabsTrigger>
<TabsTrigger value="file_manager">
{t("hosts.fileManager")}
</TabsTrigger>
<TabsTrigger value="statistics">
{t("hosts.statistics")}
</TabsTrigger>
</TabsList>
{/* Only show tabs if there's more than just the General tab (SSH has extra tabs) */}
{form.watch("connectionType") === "ssh" && (
<TabsList>
<TabsTrigger value="general">
{t("hosts.general")}
</TabsTrigger>
<TabsTrigger value="terminal">
{t("hosts.terminal")}
</TabsTrigger>
<TabsTrigger value="docker">Docker</TabsTrigger>
<TabsTrigger value="tunnel">{t("hosts.tunnel")}</TabsTrigger>
<TabsTrigger value="file_manager">
{t("hosts.fileManager")}
</TabsTrigger>
<TabsTrigger value="statistics">
{t("hosts.statistics")}
</TabsTrigger>
</TabsList>
)}
<TabsContent value="general" className="pt-2">
<FormLabel className="mb-3 font-bold">
{t("hosts.connectionType", "Connection Type")}
</FormLabel>
<div className="grid grid-cols-12 gap-4 mb-4">
<FormField
control={form.control}
name="connectionType"
render={({ field }) => (
<FormItem className="col-span-12">
<FormControl>
<div className="flex gap-2">
{[
{ value: "ssh", label: "SSH" },
{ value: "rdp", label: "RDP" },
{ value: "vnc", label: "VNC" },
{ value: "telnet", label: "Telnet" },
].map((option) => (
<Button
key={option.value}
type="button"
variant={field.value === option.value ? "default" : "outline"}
size="sm"
onClick={() => {
field.onChange(option.value);
// Update default port based on connection type
const defaultPorts: Record<string, number> = {
ssh: 22,
rdp: 3389,
vnc: 5900,
telnet: 23,
};
form.setValue("port", defaultPorts[option.value] || 22);
}}
>
{option.label}
</Button>
))}
</div>
</FormControl>
</FormItem>
)}
/>
</div>
<FormLabel className="mb-3 font-bold">
{t("hosts.connectionDetails")}
</FormLabel>
@@ -1314,6 +1385,75 @@ export function HostManagerEditor({
}}
/>
</div>
{/* RDP-specific fields */}
{form.watch("connectionType") === "rdp" && (
<>
<FormLabel className="mb-3 mt-3 font-bold">
{t("hosts.rdpSettings", "RDP Settings")}
</FormLabel>
<div className="grid grid-cols-12 gap-4">
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem className="col-span-4">
<FormLabel>{t("hosts.domain", "Domain")}</FormLabel>
<FormControl>
<Input
placeholder={t("placeholders.domain", "WORKGROUP")}
{...field}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="security"
render={({ field }) => (
<FormItem className="col-span-4">
<FormLabel>{t("hosts.security", "Security")}</FormLabel>
<Select
value={field.value || "any"}
onValueChange={field.onChange}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("hosts.selectSecurity", "Select security")} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="any">{t("hosts.securityAny", "Any")}</SelectItem>
<SelectItem value="nla">{t("hosts.securityNla", "NLA")}</SelectItem>
<SelectItem value="nla-ext">{t("hosts.securityNlaExt", "NLA Extended")}</SelectItem>
<SelectItem value="tls">{t("hosts.securityTls", "TLS")}</SelectItem>
<SelectItem value="vmconnect">{t("hosts.securityVmconnect", "VMConnect")}</SelectItem>
<SelectItem value="rdp">{t("hosts.securityRdp", "RDP")}</SelectItem>
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
control={form.control}
name="ignoreCert"
render={({ field }) => (
<FormItem className="col-span-4 flex flex-row items-center justify-between rounded-lg border p-3 mt-6">
<div className="space-y-0.5">
<FormLabel>{t("hosts.ignoreCert", "Ignore Certificate")}</FormLabel>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</>
)}
<FormLabel className="mb-3 mt-3 font-bold">
{t("hosts.organization")}
</FormLabel>
@@ -1456,49 +1596,54 @@ export function HostManagerEditor({
)}
/>
</div>
<FormLabel className="mb-3 mt-3 font-bold">
{t("hosts.authentication")}
</FormLabel>
<Tabs
value={authTab}
onValueChange={(value) => {
const newAuthType = value as
| "password"
| "key"
| "credential"
| "none";
setAuthTab(newAuthType);
form.setValue("authType", newAuthType);
}}
className="flex-1 flex flex-col h-full min-h-0"
>
<TabsList>
<TabsTrigger value="password">
{t("hosts.password")}
</TabsTrigger>
<TabsTrigger value="key">{t("hosts.key")}</TabsTrigger>
<TabsTrigger value="credential">
{t("hosts.credential")}
</TabsTrigger>
<TabsTrigger value="none">{t("hosts.none")}</TabsTrigger>
</TabsList>
<TabsContent value="password">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t("hosts.password")}</FormLabel>
<FormControl>
<PasswordInput
placeholder={t("placeholders.password")}
{...field}
/>
</FormControl>
</FormItem>
)}
/>
</TabsContent>
{/* Authentication section - only for SSH and Telnet */}
{(form.watch("connectionType") === "ssh" || form.watch("connectionType") === "telnet") && (
<>
<FormLabel className="mb-3 mt-3 font-bold">
{t("hosts.authentication")}
</FormLabel>
<Tabs
value={authTab}
onValueChange={(value) => {
const newAuthType = value as
| "password"
| "key"
| "credential"
| "none";
setAuthTab(newAuthType);
form.setValue("authType", newAuthType);
}}
className="flex-1 flex flex-col h-full min-h-0"
>
<TabsList>
<TabsTrigger value="password">
{t("hosts.password")}
</TabsTrigger>
{form.watch("connectionType") === "ssh" && (
<TabsTrigger value="key">{t("hosts.key")}</TabsTrigger>
)}
<TabsTrigger value="credential">
{t("hosts.credential")}
</TabsTrigger>
<TabsTrigger value="none">{t("hosts.none")}</TabsTrigger>
</TabsList>
<TabsContent value="password">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t("hosts.password")}</FormLabel>
<FormControl>
<PasswordInput
placeholder={t("placeholders.password")}
{...field}
/>
</FormControl>
</FormItem>
)}
/>
</TabsContent>
<TabsContent value="key">
<Tabs
value={keyInputMethod}
@@ -1847,6 +1992,33 @@ export function HostManagerEditor({
</AccordionContent>
</AccordionItem>
</Accordion>
</>
)}
{/* RDP/VNC password authentication - simpler than SSH */}
{(form.watch("connectionType") === "rdp" || form.watch("connectionType") === "vnc") && (
<>
<FormLabel className="mb-3 mt-3 font-bold">
{t("hosts.authentication")}
</FormLabel>
<div className="grid grid-cols-12 gap-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem className="col-span-12">
<FormLabel>{t("hosts.password")}</FormLabel>
<FormControl>
<PasswordInput
placeholder={t("placeholders.password")}
{...field}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</>
)}
</TabsContent>
<TabsContent value="terminal" className="space-y-1">
<FormField