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:
@@ -495,6 +495,12 @@ const migrateSchema = () => {
|
||||
);
|
||||
addColumnIfNotExists("ssh_data", "docker_config", "TEXT");
|
||||
|
||||
// Connection type columns for RDP/VNC/Telnet support
|
||||
addColumnIfNotExists("ssh_data", "connection_type", 'TEXT NOT NULL DEFAULT "ssh"');
|
||||
addColumnIfNotExists("ssh_data", "domain", "TEXT");
|
||||
addColumnIfNotExists("ssh_data", "security", "TEXT");
|
||||
addColumnIfNotExists("ssh_data", "ignore_cert", "INTEGER NOT NULL DEFAULT 0");
|
||||
|
||||
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
|
||||
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
|
||||
addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT");
|
||||
|
||||
@@ -52,6 +52,8 @@ export const sshData = sqliteTable("ssh_data", {
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
// Connection type: ssh, rdp, vnc, telnet
|
||||
connectionType: text("connection_type").notNull().default("ssh"),
|
||||
name: text("name"),
|
||||
ip: text("ip").notNull(),
|
||||
port: integer("port").notNull(),
|
||||
@@ -94,6 +96,10 @@ export const sshData = sqliteTable("ssh_data", {
|
||||
dockerConfig: text("docker_config"),
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
// RDP/VNC specific fields
|
||||
domain: text("domain"),
|
||||
security: text("security"),
|
||||
ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
|
||||
@@ -218,6 +218,7 @@ router.post(
|
||||
}
|
||||
|
||||
const {
|
||||
connectionType,
|
||||
name,
|
||||
folder,
|
||||
tags,
|
||||
@@ -244,6 +245,10 @@ router.post(
|
||||
dockerConfig,
|
||||
terminalConfig,
|
||||
forceKeyboardInteractive,
|
||||
// RDP/VNC specific fields
|
||||
domain,
|
||||
security,
|
||||
ignoreCert,
|
||||
} = hostData;
|
||||
if (
|
||||
!isNonEmptyString(userId) ||
|
||||
@@ -261,8 +266,10 @@ router.post(
|
||||
}
|
||||
|
||||
const effectiveAuthType = authType || authMethod;
|
||||
const effectiveConnectionType = connectionType || "ssh";
|
||||
const sshDataObj: Record<string, unknown> = {
|
||||
userId: userId,
|
||||
connectionType: effectiveConnectionType,
|
||||
name,
|
||||
folder: folder || null,
|
||||
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
||||
@@ -288,6 +295,10 @@ router.post(
|
||||
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
|
||||
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||
// RDP/VNC specific fields
|
||||
domain: domain || null,
|
||||
security: security || null,
|
||||
ignoreCert: ignoreCert ? 1 : 0,
|
||||
};
|
||||
|
||||
if (effectiveAuthType === "password") {
|
||||
@@ -448,6 +459,7 @@ router.put(
|
||||
}
|
||||
|
||||
const {
|
||||
connectionType,
|
||||
name,
|
||||
folder,
|
||||
tags,
|
||||
@@ -474,6 +486,10 @@ router.put(
|
||||
dockerConfig,
|
||||
terminalConfig,
|
||||
forceKeyboardInteractive,
|
||||
// RDP/VNC specific fields
|
||||
domain,
|
||||
security,
|
||||
ignoreCert,
|
||||
} = hostData;
|
||||
if (
|
||||
!isNonEmptyString(userId) ||
|
||||
@@ -494,6 +510,7 @@ router.put(
|
||||
|
||||
const effectiveAuthType = authType || authMethod;
|
||||
const sshDataObj: Record<string, unknown> = {
|
||||
connectionType: connectionType || "ssh",
|
||||
name,
|
||||
folder,
|
||||
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
||||
@@ -519,6 +536,10 @@ router.put(
|
||||
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
|
||||
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||
// RDP/VNC specific fields
|
||||
domain: domain || null,
|
||||
security: security || null,
|
||||
ignoreCert: ignoreCert ? 1 : 0,
|
||||
};
|
||||
|
||||
if (effectiveAuthType === "password") {
|
||||
|
||||
@@ -25,8 +25,11 @@ export interface DockerConfig {
|
||||
tlsKey?: string;
|
||||
}
|
||||
|
||||
export type HostConnectionType = "ssh" | "rdp" | "vnc" | "telnet";
|
||||
|
||||
export interface SSHHost {
|
||||
id: number;
|
||||
connectionType: HostConnectionType;
|
||||
name: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
@@ -59,6 +62,10 @@ export interface SSHHost {
|
||||
statsConfig?: string;
|
||||
dockerConfig?: string;
|
||||
terminalConfig?: TerminalConfig;
|
||||
// RDP/VNC specific fields
|
||||
domain?: string;
|
||||
security?: string;
|
||||
ignoreCert?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -73,6 +80,7 @@ export interface QuickActionData {
|
||||
}
|
||||
|
||||
export interface SSHHostData {
|
||||
connectionType?: HostConnectionType;
|
||||
name?: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
@@ -99,6 +107,10 @@ export interface SSHHostData {
|
||||
statsConfig?: string | Record<string, unknown>;
|
||||
dockerConfig?: DockerConfig | string;
|
||||
terminalConfig?: TerminalConfig;
|
||||
// RDP/VNC specific fields
|
||||
domain?: string;
|
||||
security?: string;
|
||||
ignoreCert?: boolean;
|
||||
}
|
||||
|
||||
export interface SSHFolder {
|
||||
@@ -370,9 +382,11 @@ export interface TabContextTab {
|
||||
terminalRef?: any;
|
||||
initialTab?: string;
|
||||
connectionConfig?: {
|
||||
type: "rdp" | "vnc" | "telnet";
|
||||
hostname: string;
|
||||
port: number;
|
||||
token: string;
|
||||
protocol: "rdp" | "vnc" | "telnet";
|
||||
type?: "rdp" | "vnc" | "telnet";
|
||||
hostname?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
domain?: string;
|
||||
|
||||
@@ -36,12 +36,10 @@ import {
|
||||
Loader2,
|
||||
Terminal,
|
||||
FolderOpen,
|
||||
Monitor,
|
||||
} from "lucide-react";
|
||||
import { Status } from "@/components/ui/shadcn-io/status";
|
||||
import { BsLightning } from "react-icons/bs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GuacamoleTestDialog } from "@/ui/desktop/apps/guacamole/GuacamoleTestDialog";
|
||||
|
||||
interface DashboardProps {
|
||||
onSelectView: (view: string) => void;
|
||||
@@ -689,22 +687,6 @@ export function Dashboard({
|
||||
{t("dashboard.userProfile")}
|
||||
</span>
|
||||
</Button>
|
||||
<GuacamoleTestDialog
|
||||
trigger={
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3 min-w-0"
|
||||
>
|
||||
<Monitor
|
||||
className="shrink-0"
|
||||
style={{ width: "40px", height: "40px" }}
|
||||
/>
|
||||
<span className="font-semibold text-sm mt-2">
|
||||
Test RDP/VNC
|
||||
</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,12 @@ import { Loader2 } from "lucide-react";
|
||||
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
||||
|
||||
export interface GuacamoleConnectionConfig {
|
||||
type: GuacamoleConnectionType;
|
||||
hostname: string;
|
||||
// Pre-fetched token (preferred) - if provided, skip token fetch
|
||||
token?: string;
|
||||
protocol?: GuacamoleConnectionType;
|
||||
// Legacy fields for backward compatibility (used if token not provided)
|
||||
type?: GuacamoleConnectionType;
|
||||
hostname?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
@@ -89,37 +93,45 @@ export const GuacamoleDisplay = forwardRef<GuacamoleDisplayHandle, GuacamoleDisp
|
||||
}));
|
||||
|
||||
const getWebSocketUrl = useCallback(async (containerWidth: number, containerHeight: number): Promise<string | null> => {
|
||||
const jwtToken = getCookie("jwt");
|
||||
if (!jwtToken) {
|
||||
setConnectionError("Authentication required");
|
||||
return null;
|
||||
}
|
||||
|
||||
// First, get an encrypted token from the backend
|
||||
try {
|
||||
const baseUrl = isDev
|
||||
? "http://localhost:30001"
|
||||
: isElectron()
|
||||
? (window as { configuredServerUrl?: string }).configuredServerUrl || "http://127.0.0.1:30001"
|
||||
: `${window.location.origin}`;
|
||||
let token: string;
|
||||
|
||||
const response = await fetch(`${baseUrl}/guacamole/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
body: JSON.stringify(connectionConfig),
|
||||
credentials: "include",
|
||||
});
|
||||
// If token is pre-fetched, use it directly
|
||||
if (connectionConfig.token) {
|
||||
token = connectionConfig.token;
|
||||
} else {
|
||||
// Otherwise, fetch token from backend (legacy behavior)
|
||||
const jwtToken = getCookie("jwt");
|
||||
if (!jwtToken) {
|
||||
setConnectionError("Authentication required");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.error || "Failed to get connection token");
|
||||
const baseUrl = isDev
|
||||
? "http://localhost:30001"
|
||||
: isElectron()
|
||||
? (window as { configuredServerUrl?: string }).configuredServerUrl || "http://127.0.0.1:30001"
|
||||
: `${window.location.origin}`;
|
||||
|
||||
const response = await fetch(`${baseUrl}/guacamole/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
body: JSON.stringify(connectionConfig),
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.error || "Failed to get connection token");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
token = data.token;
|
||||
}
|
||||
|
||||
const { token } = await response.json();
|
||||
|
||||
// Build WebSocket URL with width/height/dpi as query parameters
|
||||
// These are passed as unencrypted settings to guacamole-lite
|
||||
// Use actual container dimensions, fall back to 720p
|
||||
@@ -353,7 +365,7 @@ export const GuacamoleDisplay = forwardRef<GuacamoleDisplayHandle, GuacamoleDisp
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
<span className="text-muted-foreground">
|
||||
{t("guacamole.connecting", { type: connectionConfig.type.toUpperCase() })}
|
||||
{t("guacamole.connecting", { type: (connectionConfig.protocol || connectionConfig.type || "remote").toUpperCase() })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Monitor, MonitorPlay, Terminal } from "lucide-react";
|
||||
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext";
|
||||
import type { GuacamoleConnectionConfig } from "./GuacamoleDisplay";
|
||||
|
||||
interface GuacamoleTestDialogProps {
|
||||
trigger?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function GuacamoleTestDialog({ trigger }: GuacamoleTestDialogProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { addTab } = useTabs();
|
||||
|
||||
const [connectionType, setConnectionType] = useState<"rdp" | "vnc" | "telnet">("rdp");
|
||||
const [hostname, setHostname] = useState("");
|
||||
const [port, setPort] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [domain, setDomain] = useState("");
|
||||
const [security, setSecurity] = useState("nla");
|
||||
|
||||
const defaultPorts = { rdp: "3389", vnc: "5900", telnet: "23" };
|
||||
|
||||
const handleConnect = () => {
|
||||
if (!hostname) return;
|
||||
|
||||
const config: GuacamoleConnectionConfig = {
|
||||
type: connectionType,
|
||||
hostname,
|
||||
port: parseInt(port || defaultPorts[connectionType]),
|
||||
username: username || undefined,
|
||||
password: password || undefined,
|
||||
domain: domain || undefined,
|
||||
security: connectionType === "rdp" ? security : undefined,
|
||||
"ignore-cert": true,
|
||||
};
|
||||
|
||||
// Add a new tab for the remote desktop connection
|
||||
const tabType = connectionType === "rdp" ? "rdp" : connectionType === "vnc" ? "vnc" : "rdp";
|
||||
const title = `${connectionType.toUpperCase()} - ${hostname}`;
|
||||
|
||||
addTab({
|
||||
type: tabType,
|
||||
title,
|
||||
connectionConfig: config,
|
||||
});
|
||||
|
||||
// Close the dialog
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{trigger || (
|
||||
<Button variant="outline" className="gap-2">
|
||||
<Monitor className="w-4 h-4" />
|
||||
Test RDP/VNC
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Monitor className="w-5 h-5" />
|
||||
Remote Connection
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Tabs value={connectionType} onValueChange={(v) => {
|
||||
setConnectionType(v as "rdp" | "vnc" | "telnet");
|
||||
setPort("");
|
||||
}}>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="rdp" className="gap-1">
|
||||
<MonitorPlay className="w-4 h-4" /> RDP
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="vnc" className="gap-1">
|
||||
<Monitor className="w-4 h-4" /> VNC
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="telnet" className="gap-1">
|
||||
<Terminal className="w-4 h-4" /> Telnet
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="rdp" className="space-y-3 mt-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Hostname / IP</Label>
|
||||
<Input value={hostname} onChange={(e) => setHostname(e.target.value)} placeholder="192.168.1.100" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Port</Label>
|
||||
<Input value={port} onChange={(e) => setPort(e.target.value)} placeholder="3389" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Domain (optional)</Label>
|
||||
<Input value={domain} onChange={(e) => setDomain(e.target.value)} placeholder="WORKGROUP" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Security</Label>
|
||||
<Select value={security} onValueChange={setSecurity}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="nla">NLA (Windows 10/11)</SelectItem>
|
||||
<SelectItem value="tls">TLS</SelectItem>
|
||||
<SelectItem value="rdp">RDP (legacy)</SelectItem>
|
||||
<SelectItem value="any">Auto-negotiate</SelectItem>
|
||||
<SelectItem value="vmconnect">Hyper-V</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Username</Label>
|
||||
<Input value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Administrator" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Password</Label>
|
||||
<PasswordInput value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="vnc" className="space-y-3 mt-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Hostname / IP</Label>
|
||||
<Input value={hostname} onChange={(e) => setHostname(e.target.value)} placeholder="192.168.1.100" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Port</Label>
|
||||
<Input value={port} onChange={(e) => setPort(e.target.value)} placeholder="5900" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Password</Label>
|
||||
<PasswordInput value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="telnet" className="space-y-3 mt-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Hostname / IP</Label>
|
||||
<Input value={hostname} onChange={(e) => setHostname(e.target.value)} placeholder="192.168.1.100" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Port</Label>
|
||||
<Input value={port} onChange={(e) => setPort(e.target.value)} placeholder="23" />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Button onClick={handleConnect} disabled={!hostname} className="w-full">
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,7 +61,10 @@ import {
|
||||
HardDrive,
|
||||
Globe,
|
||||
FolderOpen,
|
||||
Monitor,
|
||||
ScreenShare,
|
||||
} from "lucide-react";
|
||||
import { getGuacamoleToken } from "@/ui/main-axios.ts";
|
||||
import type {
|
||||
SSHHost,
|
||||
SSHFolder,
|
||||
@@ -1371,7 +1374,28 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{host.enableTerminal && (
|
||||
{/* Show connection type badge */}
|
||||
{(host.connectionType === "rdp" || host.connectionType === "vnc") ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs px-1 py-0"
|
||||
>
|
||||
{host.connectionType === "rdp" ? (
|
||||
<Monitor className="h-2 w-2 mr-0.5" />
|
||||
) : (
|
||||
<ScreenShare className="h-2 w-2 mr-0.5" />
|
||||
)}
|
||||
{host.connectionType.toUpperCase()}
|
||||
</Badge>
|
||||
) : host.connectionType === "telnet" ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs px-1 py-0"
|
||||
>
|
||||
<Terminal className="h-2 w-2 mr-0.5" />
|
||||
Telnet
|
||||
</Badge>
|
||||
) : host.enableTerminal && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs px-1 py-0"
|
||||
@@ -1450,30 +1474,66 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-3 border-t border-border/50 flex items-center justify-center gap-1">
|
||||
{host.enableTerminal && (
|
||||
{/* Show connect button for SSH/Telnet if enableTerminal, or always for RDP/VNC */}
|
||||
{(host.enableTerminal || host.connectionType === "rdp" || host.connectionType === "vnc") && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
const title = host.name?.trim()
|
||||
? host.name
|
||||
: `${host.username}@${host.ip}:${host.port}`;
|
||||
addTab({
|
||||
type: "terminal",
|
||||
title,
|
||||
hostConfig: host,
|
||||
});
|
||||
const connectionType = host.connectionType || "ssh";
|
||||
|
||||
if (connectionType === "ssh" || connectionType === "telnet") {
|
||||
addTab({
|
||||
type: "terminal",
|
||||
title,
|
||||
hostConfig: host,
|
||||
});
|
||||
} else if (connectionType === "rdp" || connectionType === "vnc") {
|
||||
try {
|
||||
const tokenResponse = await getGuacamoleToken({
|
||||
protocol: connectionType,
|
||||
hostname: host.ip,
|
||||
port: host.port,
|
||||
username: host.username,
|
||||
password: host.password || "",
|
||||
domain: host.domain,
|
||||
security: host.security,
|
||||
ignoreCert: host.ignoreCert,
|
||||
});
|
||||
addTab({
|
||||
type: connectionType,
|
||||
title,
|
||||
hostConfig: host,
|
||||
connectionConfig: {
|
||||
token: tokenResponse.token,
|
||||
protocol: connectionType,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to get guacamole token for ${connectionType}:`, error);
|
||||
toast.error(`Failed to connect to ${connectionType.toUpperCase()} host`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="h-7 px-2 hover:bg-blue-500/10 hover:border-blue-500/50 flex-1"
|
||||
>
|
||||
<Terminal className="h-3.5 w-3.5" />
|
||||
{host.connectionType === "rdp" ? (
|
||||
<Monitor className="h-3.5 w-3.5" />
|
||||
) : host.connectionType === "vnc" ? (
|
||||
<ScreenShare className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Terminal className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Open Terminal</p>
|
||||
<p>{host.connectionType === "rdp" ? "Open RDP" : host.connectionType === "vnc" ? "Open VNC" : "Open Terminal"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -374,6 +374,8 @@ export function TopNavbar({
|
||||
const isSshManager = tab.type === "ssh_manager";
|
||||
const isAdmin = tab.type === "admin";
|
||||
const isUserProfile = tab.type === "user_profile";
|
||||
const isRdp = tab.type === "rdp";
|
||||
const isVnc = tab.type === "vnc";
|
||||
const isSplittable =
|
||||
isTerminal || isServer || isFileManager || isTunnel || isDocker;
|
||||
const disableSplit = !isSplittable;
|
||||
@@ -491,7 +493,9 @@ export function TopNavbar({
|
||||
isDocker ||
|
||||
isSshManager ||
|
||||
isAdmin ||
|
||||
isUserProfile
|
||||
isUserProfile ||
|
||||
isRdp ||
|
||||
isVnc
|
||||
? () => handleTabClose(tab.id)
|
||||
: undefined
|
||||
}
|
||||
@@ -507,7 +511,9 @@ export function TopNavbar({
|
||||
isDocker ||
|
||||
isSshManager ||
|
||||
isAdmin ||
|
||||
isUserProfile
|
||||
isUserProfile ||
|
||||
isRdp ||
|
||||
isVnc
|
||||
}
|
||||
disableActivate={disableActivate}
|
||||
disableSplit={disableSplit}
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
Pencil,
|
||||
ArrowDownUp,
|
||||
Container,
|
||||
Monitor,
|
||||
ScreenShare,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -18,7 +20,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext";
|
||||
import { getServerStatusById } from "@/ui/main-axios";
|
||||
import { getServerStatusById, getGuacamoleToken } from "@/ui/main-axios";
|
||||
import type { HostProps } from "../../../../types";
|
||||
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets";
|
||||
|
||||
@@ -106,8 +108,38 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
};
|
||||
}, [host.id, shouldShowStatus]);
|
||||
|
||||
const handleTerminalClick = () => {
|
||||
addTab({ type: "terminal", title, hostConfig: host });
|
||||
const handleTerminalClick = async () => {
|
||||
const connectionType = host.connectionType || "ssh";
|
||||
|
||||
if (connectionType === "ssh" || connectionType === "telnet") {
|
||||
addTab({ type: "terminal", title, hostConfig: host });
|
||||
} else if (connectionType === "rdp" || connectionType === "vnc") {
|
||||
try {
|
||||
// Get guacamole token for RDP/VNC connection
|
||||
const tokenResponse = await getGuacamoleToken({
|
||||
protocol: connectionType,
|
||||
hostname: host.ip,
|
||||
port: host.port,
|
||||
username: host.username,
|
||||
password: host.password || "",
|
||||
domain: host.domain,
|
||||
security: host.security,
|
||||
ignoreCert: host.ignoreCert,
|
||||
});
|
||||
|
||||
addTab({
|
||||
type: connectionType,
|
||||
title,
|
||||
hostConfig: host,
|
||||
connectionConfig: {
|
||||
token: tokenResponse.token,
|
||||
protocol: connectionType,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to get guacamole token for ${connectionType}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -127,13 +159,20 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
</p>
|
||||
|
||||
<ButtonGroup className="flex-shrink-0">
|
||||
{host.enableTerminal && (
|
||||
{/* Show connect button for SSH/Telnet if enableTerminal, or always for RDP/VNC */}
|
||||
{(host.enableTerminal || host.connectionType === "rdp" || host.connectionType === "vnc") && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!px-2 border-1 border-dark-border"
|
||||
onClick={handleTerminalClick}
|
||||
>
|
||||
<Terminal />
|
||||
{host.connectionType === "rdp" ? (
|
||||
<Monitor />
|
||||
) : host.connectionType === "vnc" ? (
|
||||
<ScreenShare />
|
||||
) : (
|
||||
<Terminal />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -142,7 +181,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`!px-2 border-1 border-dark-border ${
|
||||
host.enableTerminal ? "rounded-tl-none rounded-bl-none" : ""
|
||||
(host.enableTerminal || host.connectionType === "rdp" || host.connectionType === "vnc") ? "rounded-tl-none rounded-bl-none" : ""
|
||||
}`}
|
||||
>
|
||||
<EllipsisVertical />
|
||||
@@ -154,49 +193,54 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
side="right"
|
||||
className="w-56 bg-dark-bg border-dark-border text-white"
|
||||
>
|
||||
{shouldShowMetrics && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
addTab({ type: "server", title, hostConfig: host })
|
||||
}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<Server className="h-4 w-4" />
|
||||
<span className="flex-1">Open Server Stats</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableFileManager && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
addTab({ type: "file_manager", title, hostConfig: host })
|
||||
}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="flex-1">Open File Manager</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableTunnel && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
addTab({ type: "tunnel", title, hostConfig: host })
|
||||
}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<ArrowDownUp className="h-4 w-4" />
|
||||
<span className="flex-1">Open Tunnels</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableDocker && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
addTab({ type: "docker", title, hostConfig: host })
|
||||
}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<Container className="h-4 w-4" />
|
||||
<span className="flex-1">Open Docker</span>
|
||||
</DropdownMenuItem>
|
||||
{/* SSH-specific menu items */}
|
||||
{(!host.connectionType || host.connectionType === "ssh") && (
|
||||
<>
|
||||
{shouldShowMetrics && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
addTab({ type: "server", title, hostConfig: host })
|
||||
}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<Server className="h-4 w-4" />
|
||||
<span className="flex-1">Open Server Stats</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableFileManager && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
addTab({ type: "file_manager", title, hostConfig: host })
|
||||
}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="flex-1">Open File Manager</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableTunnel && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
addTab({ type: "tunnel", title, hostConfig: host })
|
||||
}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<ArrowDownUp className="h-4 w-4" />
|
||||
<span className="flex-1">Open Tunnels</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableDocker && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
addTab({ type: "docker", title, hostConfig: host })
|
||||
}
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<Container className="h-4 w-4" />
|
||||
<span className="flex-1">Open Docker</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
|
||||
@@ -838,6 +838,7 @@ export async function getSSHHosts(): Promise<SSHHost[]> {
|
||||
export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
||||
try {
|
||||
const submitData = {
|
||||
connectionType: hostData.connectionType || "ssh",
|
||||
name: hostData.name || "",
|
||||
ip: hostData.ip,
|
||||
port: parseInt(hostData.port.toString()) || 22,
|
||||
@@ -873,6 +874,10 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
||||
: null,
|
||||
terminalConfig: hostData.terminalConfig || null,
|
||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
||||
// RDP/VNC specific fields
|
||||
domain: hostData.domain || null,
|
||||
security: hostData.security || null,
|
||||
ignoreCert: Boolean(hostData.ignoreCert),
|
||||
};
|
||||
|
||||
if (!submitData.enableTunnel) {
|
||||
@@ -910,6 +915,7 @@ export async function updateSSHHost(
|
||||
): Promise<SSHHost> {
|
||||
try {
|
||||
const submitData = {
|
||||
connectionType: hostData.connectionType || "ssh",
|
||||
name: hostData.name || "",
|
||||
ip: hostData.ip,
|
||||
port: parseInt(hostData.port.toString()) || 22,
|
||||
@@ -945,6 +951,10 @@ export async function updateSSHHost(
|
||||
: null,
|
||||
terminalConfig: hostData.terminalConfig || null,
|
||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
||||
// RDP/VNC specific fields
|
||||
domain: hostData.domain || null,
|
||||
security: hostData.security || null,
|
||||
ignoreCert: Boolean(hostData.ignoreCert),
|
||||
};
|
||||
|
||||
if (!submitData.enableTunnel) {
|
||||
@@ -3121,3 +3131,40 @@ export async function unlinkOIDCFromPasswordAccount(
|
||||
throw handleApiError(error, "unlink OIDC from password account");
|
||||
}
|
||||
}
|
||||
|
||||
// Guacamole API functions
|
||||
export interface GuacamoleTokenRequest {
|
||||
protocol: "rdp" | "vnc" | "telnet";
|
||||
hostname: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
domain?: string;
|
||||
security?: string;
|
||||
ignoreCert?: boolean;
|
||||
}
|
||||
|
||||
export interface GuacamoleTokenResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export async function getGuacamoleToken(
|
||||
request: GuacamoleTokenRequest,
|
||||
): Promise<GuacamoleTokenResponse> {
|
||||
try {
|
||||
// Use authApi (port 30001 without /ssh prefix) since guacamole routes are at /guacamole
|
||||
const response = await authApi.post("/guacamole/token", {
|
||||
type: request.protocol,
|
||||
hostname: request.hostname,
|
||||
port: request.port,
|
||||
username: request.username,
|
||||
password: request.password,
|
||||
domain: request.domain,
|
||||
security: request.security,
|
||||
"ignore-cert": request.ignoreCert,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "get guacamole token");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user