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");
|
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", "private_key", "TEXT");
|
||||||
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
|
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
|
||||||
addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT");
|
addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT");
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export const sshData = sqliteTable("ssh_data", {
|
|||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
// Connection type: ssh, rdp, vnc, telnet
|
||||||
|
connectionType: text("connection_type").notNull().default("ssh"),
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
ip: text("ip").notNull(),
|
ip: text("ip").notNull(),
|
||||||
port: integer("port").notNull(),
|
port: integer("port").notNull(),
|
||||||
@@ -94,6 +96,10 @@ export const sshData = sqliteTable("ssh_data", {
|
|||||||
dockerConfig: text("docker_config"),
|
dockerConfig: text("docker_config"),
|
||||||
terminalConfig: text("terminal_config"),
|
terminalConfig: text("terminal_config"),
|
||||||
quickActions: text("quick_actions"),
|
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")
|
createdAt: text("created_at")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`CURRENT_TIMESTAMP`),
|
.default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
connectionType,
|
||||||
name,
|
name,
|
||||||
folder,
|
folder,
|
||||||
tags,
|
tags,
|
||||||
@@ -244,6 +245,10 @@ router.post(
|
|||||||
dockerConfig,
|
dockerConfig,
|
||||||
terminalConfig,
|
terminalConfig,
|
||||||
forceKeyboardInteractive,
|
forceKeyboardInteractive,
|
||||||
|
// RDP/VNC specific fields
|
||||||
|
domain,
|
||||||
|
security,
|
||||||
|
ignoreCert,
|
||||||
} = hostData;
|
} = hostData;
|
||||||
if (
|
if (
|
||||||
!isNonEmptyString(userId) ||
|
!isNonEmptyString(userId) ||
|
||||||
@@ -261,8 +266,10 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const effectiveAuthType = authType || authMethod;
|
const effectiveAuthType = authType || authMethod;
|
||||||
|
const effectiveConnectionType = connectionType || "ssh";
|
||||||
const sshDataObj: Record<string, unknown> = {
|
const sshDataObj: Record<string, unknown> = {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
connectionType: effectiveConnectionType,
|
||||||
name,
|
name,
|
||||||
folder: folder || null,
|
folder: folder || null,
|
||||||
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
||||||
@@ -288,6 +295,10 @@ router.post(
|
|||||||
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
|
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
|
||||||
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
||||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||||
|
// RDP/VNC specific fields
|
||||||
|
domain: domain || null,
|
||||||
|
security: security || null,
|
||||||
|
ignoreCert: ignoreCert ? 1 : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (effectiveAuthType === "password") {
|
if (effectiveAuthType === "password") {
|
||||||
@@ -448,6 +459,7 @@ router.put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
connectionType,
|
||||||
name,
|
name,
|
||||||
folder,
|
folder,
|
||||||
tags,
|
tags,
|
||||||
@@ -474,6 +486,10 @@ router.put(
|
|||||||
dockerConfig,
|
dockerConfig,
|
||||||
terminalConfig,
|
terminalConfig,
|
||||||
forceKeyboardInteractive,
|
forceKeyboardInteractive,
|
||||||
|
// RDP/VNC specific fields
|
||||||
|
domain,
|
||||||
|
security,
|
||||||
|
ignoreCert,
|
||||||
} = hostData;
|
} = hostData;
|
||||||
if (
|
if (
|
||||||
!isNonEmptyString(userId) ||
|
!isNonEmptyString(userId) ||
|
||||||
@@ -494,6 +510,7 @@ router.put(
|
|||||||
|
|
||||||
const effectiveAuthType = authType || authMethod;
|
const effectiveAuthType = authType || authMethod;
|
||||||
const sshDataObj: Record<string, unknown> = {
|
const sshDataObj: Record<string, unknown> = {
|
||||||
|
connectionType: connectionType || "ssh",
|
||||||
name,
|
name,
|
||||||
folder,
|
folder,
|
||||||
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
|
||||||
@@ -519,6 +536,10 @@ router.put(
|
|||||||
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
|
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
|
||||||
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
||||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||||
|
// RDP/VNC specific fields
|
||||||
|
domain: domain || null,
|
||||||
|
security: security || null,
|
||||||
|
ignoreCert: ignoreCert ? 1 : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (effectiveAuthType === "password") {
|
if (effectiveAuthType === "password") {
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ export interface DockerConfig {
|
|||||||
tlsKey?: string;
|
tlsKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HostConnectionType = "ssh" | "rdp" | "vnc" | "telnet";
|
||||||
|
|
||||||
export interface SSHHost {
|
export interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
|
connectionType: HostConnectionType;
|
||||||
name: string;
|
name: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -59,6 +62,10 @@ export interface SSHHost {
|
|||||||
statsConfig?: string;
|
statsConfig?: string;
|
||||||
dockerConfig?: string;
|
dockerConfig?: string;
|
||||||
terminalConfig?: TerminalConfig;
|
terminalConfig?: TerminalConfig;
|
||||||
|
// RDP/VNC specific fields
|
||||||
|
domain?: string;
|
||||||
|
security?: string;
|
||||||
|
ignoreCert?: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -73,6 +80,7 @@ export interface QuickActionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SSHHostData {
|
export interface SSHHostData {
|
||||||
|
connectionType?: HostConnectionType;
|
||||||
name?: string;
|
name?: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -99,6 +107,10 @@ export interface SSHHostData {
|
|||||||
statsConfig?: string | Record<string, unknown>;
|
statsConfig?: string | Record<string, unknown>;
|
||||||
dockerConfig?: DockerConfig | string;
|
dockerConfig?: DockerConfig | string;
|
||||||
terminalConfig?: TerminalConfig;
|
terminalConfig?: TerminalConfig;
|
||||||
|
// RDP/VNC specific fields
|
||||||
|
domain?: string;
|
||||||
|
security?: string;
|
||||||
|
ignoreCert?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SSHFolder {
|
export interface SSHFolder {
|
||||||
@@ -370,9 +382,11 @@ export interface TabContextTab {
|
|||||||
terminalRef?: any;
|
terminalRef?: any;
|
||||||
initialTab?: string;
|
initialTab?: string;
|
||||||
connectionConfig?: {
|
connectionConfig?: {
|
||||||
type: "rdp" | "vnc" | "telnet";
|
token: string;
|
||||||
hostname: string;
|
protocol: "rdp" | "vnc" | "telnet";
|
||||||
port: number;
|
type?: "rdp" | "vnc" | "telnet";
|
||||||
|
hostname?: string;
|
||||||
|
port?: number;
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
|
|||||||
@@ -36,12 +36,10 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Terminal,
|
Terminal,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Monitor,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Status } from "@/components/ui/shadcn-io/status";
|
import { Status } from "@/components/ui/shadcn-io/status";
|
||||||
import { BsLightning } from "react-icons/bs";
|
import { BsLightning } from "react-icons/bs";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { GuacamoleTestDialog } from "@/ui/desktop/apps/guacamole/GuacamoleTestDialog";
|
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
onSelectView: (view: string) => void;
|
onSelectView: (view: string) => void;
|
||||||
@@ -689,22 +687,6 @@ export function Dashboard({
|
|||||||
{t("dashboard.userProfile")}
|
{t("dashboard.userProfile")}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,8 +15,12 @@ import { Loader2 } from "lucide-react";
|
|||||||
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
||||||
|
|
||||||
export interface GuacamoleConnectionConfig {
|
export interface GuacamoleConnectionConfig {
|
||||||
type: GuacamoleConnectionType;
|
// Pre-fetched token (preferred) - if provided, skip token fetch
|
||||||
hostname: string;
|
token?: string;
|
||||||
|
protocol?: GuacamoleConnectionType;
|
||||||
|
// Legacy fields for backward compatibility (used if token not provided)
|
||||||
|
type?: GuacamoleConnectionType;
|
||||||
|
hostname?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
@@ -89,14 +93,20 @@ export const GuacamoleDisplay = forwardRef<GuacamoleDisplayHandle, GuacamoleDisp
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const getWebSocketUrl = useCallback(async (containerWidth: number, containerHeight: number): Promise<string | null> => {
|
const getWebSocketUrl = useCallback(async (containerWidth: number, containerHeight: number): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
let token: string;
|
||||||
|
|
||||||
|
// 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");
|
const jwtToken = getCookie("jwt");
|
||||||
if (!jwtToken) {
|
if (!jwtToken) {
|
||||||
setConnectionError("Authentication required");
|
setConnectionError("Authentication required");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, get an encrypted token from the backend
|
|
||||||
try {
|
|
||||||
const baseUrl = isDev
|
const baseUrl = isDev
|
||||||
? "http://localhost:30001"
|
? "http://localhost:30001"
|
||||||
: isElectron()
|
: isElectron()
|
||||||
@@ -118,7 +128,9 @@ export const GuacamoleDisplay = forwardRef<GuacamoleDisplayHandle, GuacamoleDisp
|
|||||||
throw new Error(err.error || "Failed to get connection token");
|
throw new Error(err.error || "Failed to get connection token");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token } = await response.json();
|
const data = await response.json();
|
||||||
|
token = data.token;
|
||||||
|
}
|
||||||
|
|
||||||
// Build WebSocket URL with width/height/dpi as query parameters
|
// Build WebSocket URL with width/height/dpi as query parameters
|
||||||
// These are passed as unencrypted settings to guacamole-lite
|
// These are passed as unencrypted settings to guacamole-lite
|
||||||
@@ -353,7 +365,7 @@ export const GuacamoleDisplay = forwardRef<GuacamoleDisplayHandle, GuacamoleDisp
|
|||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{t("guacamole.connecting", { type: connectionConfig.type.toUpperCase() })}
|
{t("guacamole.connecting", { type: (connectionConfig.protocol || connectionConfig.type || "remote").toUpperCase() })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
connectionType: z.enum(["ssh", "rdp", "vnc", "telnet"]).default("ssh"),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
ip: z.string().min(1),
|
ip: z.string().min(1),
|
||||||
port: z.coerce.number().min(1).max(65535),
|
port: z.coerce.number().min(1).max(65535),
|
||||||
@@ -443,6 +444,10 @@ export function HostManagerEditor({
|
|||||||
tags: z.array(z.string().min(1)).default([]),
|
tags: z.array(z.string().min(1)).default([]),
|
||||||
pin: z.boolean().default(false),
|
pin: z.boolean().default(false),
|
||||||
authType: z.enum(["password", "key", "credential", "none"]),
|
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(),
|
credentialId: z.number().optional().nullable(),
|
||||||
overrideCredentialUsername: z.boolean().optional(),
|
overrideCredentialUsername: z.boolean().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
@@ -648,6 +653,7 @@ export function HostManagerEditor({
|
|||||||
resolver: zodResolver(formSchema) as any,
|
resolver: zodResolver(formSchema) as any,
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
|
connectionType: "ssh" as const,
|
||||||
ip: "",
|
ip: "",
|
||||||
port: 22,
|
port: 22,
|
||||||
username: "",
|
username: "",
|
||||||
@@ -682,6 +688,10 @@ export function HostManagerEditor({
|
|||||||
tlsCert: "",
|
tlsCert: "",
|
||||||
tlsKey: "",
|
tlsKey: "",
|
||||||
},
|
},
|
||||||
|
// RDP/VNC specific defaults
|
||||||
|
domain: "",
|
||||||
|
security: "",
|
||||||
|
ignoreCert: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -759,6 +769,7 @@ export function HostManagerEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
|
connectionType: (cleanedHost.connectionType || "ssh") as "ssh" | "rdp" | "vnc" | "telnet",
|
||||||
name: cleanedHost.name || "",
|
name: cleanedHost.name || "",
|
||||||
ip: cleanedHost.ip || "",
|
ip: cleanedHost.ip || "",
|
||||||
port: cleanedHost.port || 22,
|
port: cleanedHost.port || 22,
|
||||||
@@ -801,6 +812,10 @@ export function HostManagerEditor({
|
|||||||
forceKeyboardInteractive: Boolean(cleanedHost.forceKeyboardInteractive),
|
forceKeyboardInteractive: Boolean(cleanedHost.forceKeyboardInteractive),
|
||||||
enableDocker: Boolean(cleanedHost.enableDocker),
|
enableDocker: Boolean(cleanedHost.enableDocker),
|
||||||
dockerConfig: parsedDockerConfig,
|
dockerConfig: parsedDockerConfig,
|
||||||
|
// RDP/VNC specific fields
|
||||||
|
domain: cleanedHost.domain || "",
|
||||||
|
security: cleanedHost.security || "",
|
||||||
|
ignoreCert: Boolean(cleanedHost.ignoreCert),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (defaultAuthType === "password") {
|
if (defaultAuthType === "password") {
|
||||||
@@ -828,6 +843,7 @@ export function HostManagerEditor({
|
|||||||
} else {
|
} else {
|
||||||
setAuthTab("password");
|
setAuthTab("password");
|
||||||
const defaultFormData = {
|
const defaultFormData = {
|
||||||
|
connectionType: "ssh" as const,
|
||||||
name: "",
|
name: "",
|
||||||
ip: "",
|
ip: "",
|
||||||
port: 22,
|
port: 22,
|
||||||
@@ -863,6 +879,10 @@ export function HostManagerEditor({
|
|||||||
tlsCert: "",
|
tlsCert: "",
|
||||||
tlsKey: "",
|
tlsKey: "",
|
||||||
},
|
},
|
||||||
|
// RDP/VNC specific defaults
|
||||||
|
domain: "",
|
||||||
|
security: "",
|
||||||
|
ignoreCert: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
form.reset(defaultFormData);
|
form.reset(defaultFormData);
|
||||||
@@ -910,6 +930,7 @@ export function HostManagerEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submitData: Record<string, unknown> = {
|
const submitData: Record<string, unknown> = {
|
||||||
|
connectionType: data.connectionType || "ssh",
|
||||||
name: data.name,
|
name: data.name,
|
||||||
ip: data.ip,
|
ip: data.ip,
|
||||||
port: data.port,
|
port: data.port,
|
||||||
@@ -931,6 +952,10 @@ export function HostManagerEditor({
|
|||||||
statsConfig: data.statsConfig || DEFAULT_STATS_CONFIG,
|
statsConfig: data.statsConfig || DEFAULT_STATS_CONFIG,
|
||||||
terminalConfig: data.terminalConfig || DEFAULT_TERMINAL_CONFIG,
|
terminalConfig: data.terminalConfig || DEFAULT_TERMINAL_CONFIG,
|
||||||
forceKeyboardInteractive: Boolean(data.forceKeyboardInteractive),
|
forceKeyboardInteractive: Boolean(data.forceKeyboardInteractive),
|
||||||
|
// RDP/VNC specific fields
|
||||||
|
domain: data.domain || null,
|
||||||
|
security: data.security || null,
|
||||||
|
ignoreCert: Boolean(data.ignoreCert),
|
||||||
};
|
};
|
||||||
|
|
||||||
submitData.credentialId = null;
|
submitData.credentialId = null;
|
||||||
@@ -1230,6 +1255,8 @@ export function HostManagerEditor({
|
|||||||
onValueChange={setActiveTab}
|
onValueChange={setActiveTab}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
|
{/* Only show tabs if there's more than just the General tab (SSH has extra tabs) */}
|
||||||
|
{form.watch("connectionType") === "ssh" && (
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="general">
|
<TabsTrigger value="general">
|
||||||
{t("hosts.general")}
|
{t("hosts.general")}
|
||||||
@@ -1246,7 +1273,51 @@ export function HostManagerEditor({
|
|||||||
{t("hosts.statistics")}
|
{t("hosts.statistics")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
)}
|
||||||
<TabsContent value="general" className="pt-2">
|
<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">
|
<FormLabel className="mb-3 font-bold">
|
||||||
{t("hosts.connectionDetails")}
|
{t("hosts.connectionDetails")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
@@ -1314,6 +1385,75 @@ export function HostManagerEditor({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<FormLabel className="mb-3 mt-3 font-bold">
|
||||||
{t("hosts.organization")}
|
{t("hosts.organization")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
@@ -1456,6 +1596,9 @@ export function HostManagerEditor({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Authentication section - only for SSH and Telnet */}
|
||||||
|
{(form.watch("connectionType") === "ssh" || form.watch("connectionType") === "telnet") && (
|
||||||
|
<>
|
||||||
<FormLabel className="mb-3 mt-3 font-bold">
|
<FormLabel className="mb-3 mt-3 font-bold">
|
||||||
{t("hosts.authentication")}
|
{t("hosts.authentication")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
@@ -1476,7 +1619,9 @@ export function HostManagerEditor({
|
|||||||
<TabsTrigger value="password">
|
<TabsTrigger value="password">
|
||||||
{t("hosts.password")}
|
{t("hosts.password")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
{form.watch("connectionType") === "ssh" && (
|
||||||
<TabsTrigger value="key">{t("hosts.key")}</TabsTrigger>
|
<TabsTrigger value="key">{t("hosts.key")}</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="credential">
|
<TabsTrigger value="credential">
|
||||||
{t("hosts.credential")}
|
{t("hosts.credential")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -1847,6 +1992,33 @@ export function HostManagerEditor({
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</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>
|
||||||
<TabsContent value="terminal" className="space-y-1">
|
<TabsContent value="terminal" className="space-y-1">
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ import {
|
|||||||
HardDrive,
|
HardDrive,
|
||||||
Globe,
|
Globe,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
|
Monitor,
|
||||||
|
ScreenShare,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { getGuacamoleToken } from "@/ui/main-axios.ts";
|
||||||
import type {
|
import type {
|
||||||
SSHHost,
|
SSHHost,
|
||||||
SSHFolder,
|
SSHFolder,
|
||||||
@@ -1371,7 +1374,28 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-1">
|
<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
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-xs px-1 py-0"
|
className="text-xs px-1 py-0"
|
||||||
@@ -1450,30 +1474,66 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-border/50 flex items-center justify-center gap-1">
|
<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>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={(e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const title = host.name?.trim()
|
const title = host.name?.trim()
|
||||||
? host.name
|
? host.name
|
||||||
: `${host.username}@${host.ip}:${host.port}`;
|
: `${host.username}@${host.ip}:${host.port}`;
|
||||||
|
const connectionType = host.connectionType || "ssh";
|
||||||
|
|
||||||
|
if (connectionType === "ssh" || connectionType === "telnet") {
|
||||||
addTab({
|
addTab({
|
||||||
type: "terminal",
|
type: "terminal",
|
||||||
title,
|
title,
|
||||||
hostConfig: host,
|
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"
|
className="h-7 px-2 hover:bg-blue-500/10 hover:border-blue-500/50 flex-1"
|
||||||
>
|
>
|
||||||
|
{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" />
|
<Terminal className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Open Terminal</p>
|
<p>{host.connectionType === "rdp" ? "Open RDP" : host.connectionType === "vnc" ? "Open VNC" : "Open Terminal"}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -374,6 +374,8 @@ export function TopNavbar({
|
|||||||
const isSshManager = tab.type === "ssh_manager";
|
const isSshManager = tab.type === "ssh_manager";
|
||||||
const isAdmin = tab.type === "admin";
|
const isAdmin = tab.type === "admin";
|
||||||
const isUserProfile = tab.type === "user_profile";
|
const isUserProfile = tab.type === "user_profile";
|
||||||
|
const isRdp = tab.type === "rdp";
|
||||||
|
const isVnc = tab.type === "vnc";
|
||||||
const isSplittable =
|
const isSplittable =
|
||||||
isTerminal || isServer || isFileManager || isTunnel || isDocker;
|
isTerminal || isServer || isFileManager || isTunnel || isDocker;
|
||||||
const disableSplit = !isSplittable;
|
const disableSplit = !isSplittable;
|
||||||
@@ -491,7 +493,9 @@ export function TopNavbar({
|
|||||||
isDocker ||
|
isDocker ||
|
||||||
isSshManager ||
|
isSshManager ||
|
||||||
isAdmin ||
|
isAdmin ||
|
||||||
isUserProfile
|
isUserProfile ||
|
||||||
|
isRdp ||
|
||||||
|
isVnc
|
||||||
? () => handleTabClose(tab.id)
|
? () => handleTabClose(tab.id)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -507,7 +511,9 @@ export function TopNavbar({
|
|||||||
isDocker ||
|
isDocker ||
|
||||||
isSshManager ||
|
isSshManager ||
|
||||||
isAdmin ||
|
isAdmin ||
|
||||||
isUserProfile
|
isUserProfile ||
|
||||||
|
isRdp ||
|
||||||
|
isVnc
|
||||||
}
|
}
|
||||||
disableActivate={disableActivate}
|
disableActivate={disableActivate}
|
||||||
disableSplit={disableSplit}
|
disableSplit={disableSplit}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
Pencil,
|
Pencil,
|
||||||
ArrowDownUp,
|
ArrowDownUp,
|
||||||
Container,
|
Container,
|
||||||
|
Monitor,
|
||||||
|
ScreenShare,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -18,7 +20,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext";
|
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 type { HostProps } from "../../../../types";
|
||||||
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets";
|
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets";
|
||||||
|
|
||||||
@@ -106,8 +108,38 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
|||||||
};
|
};
|
||||||
}, [host.id, shouldShowStatus]);
|
}, [host.id, shouldShowStatus]);
|
||||||
|
|
||||||
const handleTerminalClick = () => {
|
const handleTerminalClick = async () => {
|
||||||
|
const connectionType = host.connectionType || "ssh";
|
||||||
|
|
||||||
|
if (connectionType === "ssh" || connectionType === "telnet") {
|
||||||
addTab({ type: "terminal", title, hostConfig: host });
|
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 (
|
return (
|
||||||
@@ -127,13 +159,20 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ButtonGroup className="flex-shrink-0">
|
<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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="!px-2 border-1 border-dark-border"
|
className="!px-2 border-1 border-dark-border"
|
||||||
onClick={handleTerminalClick}
|
onClick={handleTerminalClick}
|
||||||
>
|
>
|
||||||
|
{host.connectionType === "rdp" ? (
|
||||||
|
<Monitor />
|
||||||
|
) : host.connectionType === "vnc" ? (
|
||||||
|
<ScreenShare />
|
||||||
|
) : (
|
||||||
<Terminal />
|
<Terminal />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -142,7 +181,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`!px-2 border-1 border-dark-border ${
|
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 />
|
<EllipsisVertical />
|
||||||
@@ -154,6 +193,9 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
|||||||
side="right"
|
side="right"
|
||||||
className="w-56 bg-dark-bg border-dark-border text-white"
|
className="w-56 bg-dark-bg border-dark-border text-white"
|
||||||
>
|
>
|
||||||
|
{/* SSH-specific menu items */}
|
||||||
|
{(!host.connectionType || host.connectionType === "ssh") && (
|
||||||
|
<>
|
||||||
{shouldShowMetrics && (
|
{shouldShowMetrics && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -198,6 +240,8 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
|||||||
<span className="flex-1">Open Docker</span>
|
<span className="flex-1">Open Docker</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
addTab({
|
addTab({
|
||||||
|
|||||||
@@ -838,6 +838,7 @@ export async function getSSHHosts(): Promise<SSHHost[]> {
|
|||||||
export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
||||||
try {
|
try {
|
||||||
const submitData = {
|
const submitData = {
|
||||||
|
connectionType: hostData.connectionType || "ssh",
|
||||||
name: hostData.name || "",
|
name: hostData.name || "",
|
||||||
ip: hostData.ip,
|
ip: hostData.ip,
|
||||||
port: parseInt(hostData.port.toString()) || 22,
|
port: parseInt(hostData.port.toString()) || 22,
|
||||||
@@ -873,6 +874,10 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
|||||||
: null,
|
: null,
|
||||||
terminalConfig: hostData.terminalConfig || null,
|
terminalConfig: hostData.terminalConfig || null,
|
||||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
||||||
|
// RDP/VNC specific fields
|
||||||
|
domain: hostData.domain || null,
|
||||||
|
security: hostData.security || null,
|
||||||
|
ignoreCert: Boolean(hostData.ignoreCert),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!submitData.enableTunnel) {
|
if (!submitData.enableTunnel) {
|
||||||
@@ -910,6 +915,7 @@ export async function updateSSHHost(
|
|||||||
): Promise<SSHHost> {
|
): Promise<SSHHost> {
|
||||||
try {
|
try {
|
||||||
const submitData = {
|
const submitData = {
|
||||||
|
connectionType: hostData.connectionType || "ssh",
|
||||||
name: hostData.name || "",
|
name: hostData.name || "",
|
||||||
ip: hostData.ip,
|
ip: hostData.ip,
|
||||||
port: parseInt(hostData.port.toString()) || 22,
|
port: parseInt(hostData.port.toString()) || 22,
|
||||||
@@ -945,6 +951,10 @@ export async function updateSSHHost(
|
|||||||
: null,
|
: null,
|
||||||
terminalConfig: hostData.terminalConfig || null,
|
terminalConfig: hostData.terminalConfig || null,
|
||||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
||||||
|
// RDP/VNC specific fields
|
||||||
|
domain: hostData.domain || null,
|
||||||
|
security: hostData.security || null,
|
||||||
|
ignoreCert: Boolean(hostData.ignoreCert),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!submitData.enableTunnel) {
|
if (!submitData.enableTunnel) {
|
||||||
@@ -3121,3 +3131,40 @@ export async function unlinkOIDCFromPasswordAccount(
|
|||||||
throw handleApiError(error, "unlink OIDC from password account");
|
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