feat: Enhance Guacamole integration with extended configuration options
- Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings. - Implemented logging for token requests and received options for better debugging. - Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values. - Integrated Guacamole configuration parsing in HostManagerViewer and Host components. - Enhanced API requests to include extended Guacamole configuration parameters in the token request. - Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API.
This commit is contained in:
@@ -500,6 +500,7 @@ const migrateSchema = () => {
|
||||
addColumnIfNotExists("ssh_data", "domain", "TEXT");
|
||||
addColumnIfNotExists("ssh_data", "security", "TEXT");
|
||||
addColumnIfNotExists("ssh_data", "ignore_cert", "INTEGER NOT NULL DEFAULT 0");
|
||||
addColumnIfNotExists("ssh_data", "guacamole_config", "TEXT");
|
||||
|
||||
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
|
||||
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
|
||||
|
||||
@@ -100,6 +100,8 @@ export const sshData = sqliteTable("ssh_data", {
|
||||
domain: text("domain"),
|
||||
security: text("security"),
|
||||
ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false),
|
||||
// RDP/VNC extended configuration (stored as JSON)
|
||||
guacamoleConfig: text("guacamole_config"),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
|
||||
@@ -249,6 +249,7 @@ router.post(
|
||||
domain,
|
||||
security,
|
||||
ignoreCert,
|
||||
guacamoleConfig,
|
||||
} = hostData;
|
||||
if (
|
||||
!isNonEmptyString(userId) ||
|
||||
@@ -299,6 +300,7 @@ router.post(
|
||||
domain: domain || null,
|
||||
security: security || null,
|
||||
ignoreCert: ignoreCert ? 1 : 0,
|
||||
guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
|
||||
};
|
||||
|
||||
if (effectiveAuthType === "password") {
|
||||
@@ -363,6 +365,9 @@ router.post(
|
||||
dockerConfig: createdHost.dockerConfig
|
||||
? JSON.parse(createdHost.dockerConfig as string)
|
||||
: undefined,
|
||||
guacamoleConfig: createdHost.guacamoleConfig
|
||||
? JSON.parse(createdHost.guacamoleConfig as string)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
|
||||
@@ -490,6 +495,7 @@ router.put(
|
||||
domain,
|
||||
security,
|
||||
ignoreCert,
|
||||
guacamoleConfig,
|
||||
} = hostData;
|
||||
if (
|
||||
!isNonEmptyString(userId) ||
|
||||
@@ -540,6 +546,7 @@ router.put(
|
||||
domain: domain || null,
|
||||
security: security || null,
|
||||
ignoreCert: ignoreCert ? 1 : 0,
|
||||
guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
|
||||
};
|
||||
|
||||
if (effectiveAuthType === "password") {
|
||||
@@ -622,6 +629,9 @@ router.put(
|
||||
dockerConfig: updatedHost.dockerConfig
|
||||
? JSON.parse(updatedHost.dockerConfig as string)
|
||||
: undefined,
|
||||
guacamoleConfig: updatedHost.guacamoleConfig
|
||||
? JSON.parse(updatedHost.guacamoleConfig as string)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
|
||||
@@ -730,6 +740,9 @@ router.get(
|
||||
terminalConfig: row.terminalConfig
|
||||
? JSON.parse(row.terminalConfig as string)
|
||||
: undefined,
|
||||
guacamoleConfig: row.guacamoleConfig
|
||||
? JSON.parse(row.guacamoleConfig as string)
|
||||
: undefined,
|
||||
forceKeyboardInteractive: row.forceKeyboardInteractive === "true",
|
||||
};
|
||||
|
||||
@@ -805,6 +818,9 @@ router.get(
|
||||
terminalConfig: host.terminalConfig
|
||||
? JSON.parse(host.terminalConfig)
|
||||
: undefined,
|
||||
guacamoleConfig: host.guacamoleConfig
|
||||
? JSON.parse(host.guacamoleConfig)
|
||||
: undefined,
|
||||
forceKeyboardInteractive: host.forceKeyboardInteractive === "true",
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,24 @@ router.post("/token", async (req, res) => {
|
||||
return res.status(400).json({ error: "Invalid connection type. Must be rdp, vnc, or telnet" });
|
||||
}
|
||||
|
||||
// Log received options for debugging
|
||||
guacLogger.info("Guacamole token request received", {
|
||||
operation: "guac_token_request",
|
||||
type,
|
||||
hostname,
|
||||
port,
|
||||
optionKeys: Object.keys(options),
|
||||
optionsCount: Object.keys(options).length,
|
||||
});
|
||||
|
||||
// Log specific option values for debugging
|
||||
if (Object.keys(options).length > 0) {
|
||||
guacLogger.info("Guacamole options received", {
|
||||
operation: "guac_token_options",
|
||||
options: JSON.stringify(options),
|
||||
});
|
||||
}
|
||||
|
||||
let token: string;
|
||||
|
||||
switch (type) {
|
||||
|
||||
@@ -27,6 +27,97 @@ export interface DockerConfig {
|
||||
|
||||
export type HostConnectionType = "ssh" | "rdp" | "vnc" | "telnet";
|
||||
|
||||
// Guacamole configuration for RDP/VNC/Telnet connections
|
||||
export interface GuacamoleConfig {
|
||||
// Display settings
|
||||
colorDepth?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
dpi?: number;
|
||||
resizeMethod?: string;
|
||||
forceLossless?: boolean;
|
||||
// Audio settings
|
||||
disableAudio?: boolean;
|
||||
enableAudioInput?: boolean;
|
||||
// RDP Performance settings
|
||||
enableWallpaper?: boolean;
|
||||
enableTheming?: boolean;
|
||||
enableFontSmoothing?: boolean;
|
||||
enableFullWindowDrag?: boolean;
|
||||
enableDesktopComposition?: boolean;
|
||||
enableMenuAnimations?: boolean;
|
||||
disableBitmapCaching?: boolean;
|
||||
disableOffscreenCaching?: boolean;
|
||||
disableGlyphCaching?: boolean;
|
||||
disableGfx?: boolean;
|
||||
// RDP Device redirection
|
||||
enablePrinting?: boolean;
|
||||
printerName?: string;
|
||||
enableDrive?: boolean;
|
||||
driveName?: string;
|
||||
drivePath?: string;
|
||||
createDrivePath?: boolean;
|
||||
disableDownload?: boolean;
|
||||
disableUpload?: boolean;
|
||||
enableTouch?: boolean;
|
||||
// RDP Session settings
|
||||
clientName?: string;
|
||||
console?: boolean;
|
||||
initialProgram?: string;
|
||||
serverLayout?: string;
|
||||
timezone?: string;
|
||||
// RDP Gateway settings
|
||||
gatewayHostname?: string;
|
||||
gatewayPort?: number;
|
||||
gatewayUsername?: string;
|
||||
gatewayPassword?: string;
|
||||
gatewayDomain?: string;
|
||||
// RDP RemoteApp settings
|
||||
remoteApp?: string;
|
||||
remoteAppDir?: string;
|
||||
remoteAppArgs?: string;
|
||||
// RDP Preconnection settings (Hyper-V)
|
||||
preconnectionId?: number;
|
||||
preconnectionBlob?: string;
|
||||
// RDP Load balancing
|
||||
loadBalanceInfo?: string;
|
||||
// Clipboard settings
|
||||
normalizeClipboard?: string;
|
||||
disableCopy?: boolean;
|
||||
disablePaste?: boolean;
|
||||
// VNC specific settings
|
||||
cursor?: string;
|
||||
swapRedBlue?: boolean;
|
||||
readOnly?: boolean;
|
||||
// VNC Repeater settings
|
||||
destHost?: string;
|
||||
destPort?: number;
|
||||
// VNC Reverse connection
|
||||
reverseConnect?: boolean;
|
||||
listenTimeout?: number;
|
||||
// Common SFTP settings (for RDP/VNC file transfer)
|
||||
enableSftp?: boolean;
|
||||
sftpHostname?: string;
|
||||
sftpPort?: number;
|
||||
sftpUsername?: string;
|
||||
sftpPassword?: string;
|
||||
sftpPrivateKey?: string;
|
||||
sftpDirectory?: string;
|
||||
// Recording settings
|
||||
recordingPath?: string;
|
||||
recordingName?: string;
|
||||
createRecordingPath?: boolean;
|
||||
recordingExcludeOutput?: boolean;
|
||||
recordingExcludeMouse?: boolean;
|
||||
recordingIncludeKeys?: boolean;
|
||||
// Wake-on-LAN settings
|
||||
wolSendPacket?: boolean;
|
||||
wolMacAddr?: string;
|
||||
wolBroadcastAddr?: string;
|
||||
wolUdpPort?: number;
|
||||
wolWaitTime?: number;
|
||||
}
|
||||
|
||||
export interface SSHHost {
|
||||
id: number;
|
||||
connectionType: HostConnectionType;
|
||||
@@ -62,10 +153,12 @@ export interface SSHHost {
|
||||
statsConfig?: string;
|
||||
dockerConfig?: string;
|
||||
terminalConfig?: TerminalConfig;
|
||||
// RDP/VNC specific fields
|
||||
// RDP/VNC specific fields (basic)
|
||||
domain?: string;
|
||||
security?: string;
|
||||
ignoreCert?: boolean;
|
||||
// RDP/VNC extended configuration (stored as JSON)
|
||||
guacamoleConfig?: GuacamoleConfig | string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -107,10 +200,12 @@ export interface SSHHostData {
|
||||
statsConfig?: string | Record<string, unknown>;
|
||||
dockerConfig?: DockerConfig | string;
|
||||
terminalConfig?: TerminalConfig;
|
||||
// RDP/VNC specific fields
|
||||
// RDP/VNC specific fields (basic)
|
||||
domain?: string;
|
||||
security?: string;
|
||||
ignoreCert?: boolean;
|
||||
// RDP/VNC extended configuration
|
||||
guacamoleConfig?: GuacamoleConfig;
|
||||
}
|
||||
|
||||
export interface SSHFolder {
|
||||
|
||||
@@ -78,7 +78,7 @@ import {
|
||||
DEFAULT_TERMINAL_CONFIG,
|
||||
} from "@/constants/terminal-themes";
|
||||
import { TerminalPreview } from "@/ui/desktop/apps/terminal/TerminalPreview.tsx";
|
||||
import type { TerminalConfig } from "@/types";
|
||||
import type { TerminalConfig, SSHHost } from "@/types";
|
||||
import { Plus, X, Check, ChevronsUpDown } from "lucide-react";
|
||||
|
||||
interface JumpHostItemProps {
|
||||
@@ -277,46 +277,6 @@ function QuickActionItem({
|
||||
);
|
||||
}
|
||||
|
||||
interface SSHHost {
|
||||
id: number;
|
||||
name: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
username: string;
|
||||
folder: string;
|
||||
tags: string[];
|
||||
pin: boolean;
|
||||
authType: string;
|
||||
password?: string;
|
||||
key?: string;
|
||||
keyPassword?: string;
|
||||
keyType?: string;
|
||||
enableTerminal: boolean;
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: Array<{
|
||||
sourcePort: number;
|
||||
endpointPort: number;
|
||||
endpointHost: string;
|
||||
maxRetries: number;
|
||||
retryInterval: number;
|
||||
autoStart: boolean;
|
||||
}>;
|
||||
jumpHosts?: Array<{
|
||||
hostId: number;
|
||||
}>;
|
||||
quickActions?: Array<{
|
||||
name: string;
|
||||
snippetId: number;
|
||||
}>;
|
||||
statsConfig?: StatsConfig;
|
||||
terminalConfig?: TerminalConfig;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
credentialId?: number;
|
||||
}
|
||||
|
||||
interface SSHManagerHostEditorProps {
|
||||
editingHost?: SSHHost | null;
|
||||
onFormSubmit?: (updatedHost?: SSHHost) => void;
|
||||
@@ -448,6 +408,77 @@ export function HostManagerEditor({
|
||||
domain: z.string().optional(),
|
||||
security: z.string().optional(),
|
||||
ignoreCert: z.boolean().default(false),
|
||||
// RDP/VNC extended configuration
|
||||
guacamoleConfig: z.object({
|
||||
// Display settings
|
||||
colorDepth: z.coerce.number().optional(),
|
||||
width: z.coerce.number().optional(),
|
||||
height: z.coerce.number().optional(),
|
||||
dpi: z.coerce.number().optional(),
|
||||
resizeMethod: z.string().optional(),
|
||||
forceLossless: z.boolean().optional(),
|
||||
// Audio settings
|
||||
disableAudio: z.boolean().optional(),
|
||||
enableAudioInput: z.boolean().optional(),
|
||||
// RDP Performance settings
|
||||
enableWallpaper: z.boolean().optional(),
|
||||
enableTheming: z.boolean().optional(),
|
||||
enableFontSmoothing: z.boolean().optional(),
|
||||
enableFullWindowDrag: z.boolean().optional(),
|
||||
enableDesktopComposition: z.boolean().optional(),
|
||||
enableMenuAnimations: z.boolean().optional(),
|
||||
disableBitmapCaching: z.boolean().optional(),
|
||||
disableOffscreenCaching: z.boolean().optional(),
|
||||
disableGlyphCaching: z.boolean().optional(),
|
||||
disableGfx: z.boolean().optional(),
|
||||
// RDP Device redirection
|
||||
enablePrinting: z.boolean().optional(),
|
||||
printerName: z.string().optional(),
|
||||
enableDrive: z.boolean().optional(),
|
||||
driveName: z.string().optional(),
|
||||
drivePath: z.string().optional(),
|
||||
createDrivePath: z.boolean().optional(),
|
||||
disableDownload: z.boolean().optional(),
|
||||
disableUpload: z.boolean().optional(),
|
||||
enableTouch: z.boolean().optional(),
|
||||
// RDP Session settings
|
||||
clientName: z.string().optional(),
|
||||
console: z.boolean().optional(),
|
||||
initialProgram: z.string().optional(),
|
||||
serverLayout: z.string().optional(),
|
||||
timezone: z.string().optional(),
|
||||
// RDP Gateway settings
|
||||
gatewayHostname: z.string().optional(),
|
||||
gatewayPort: z.coerce.number().optional(),
|
||||
gatewayUsername: z.string().optional(),
|
||||
gatewayPassword: z.string().optional(),
|
||||
gatewayDomain: z.string().optional(),
|
||||
// RDP RemoteApp settings
|
||||
remoteApp: z.string().optional(),
|
||||
remoteAppDir: z.string().optional(),
|
||||
remoteAppArgs: z.string().optional(),
|
||||
// Clipboard settings
|
||||
normalizeClipboard: z.string().optional(),
|
||||
disableCopy: z.boolean().optional(),
|
||||
disablePaste: z.boolean().optional(),
|
||||
// VNC specific settings
|
||||
cursor: z.string().optional(),
|
||||
swapRedBlue: z.boolean().optional(),
|
||||
readOnly: z.boolean().optional(),
|
||||
// Recording settings
|
||||
recordingPath: z.string().optional(),
|
||||
recordingName: z.string().optional(),
|
||||
createRecordingPath: z.boolean().optional(),
|
||||
recordingExcludeOutput: z.boolean().optional(),
|
||||
recordingExcludeMouse: z.boolean().optional(),
|
||||
recordingIncludeKeys: z.boolean().optional(),
|
||||
// Wake-on-LAN settings
|
||||
wolSendPacket: z.boolean().optional(),
|
||||
wolMacAddr: z.string().optional(),
|
||||
wolBroadcastAddr: z.string().optional(),
|
||||
wolUdpPort: z.coerce.number().optional(),
|
||||
wolWaitTime: z.coerce.number().optional(),
|
||||
}).optional(),
|
||||
credentialId: z.number().optional().nullable(),
|
||||
overrideCredentialUsername: z.boolean().optional(),
|
||||
password: z.string().optional(),
|
||||
@@ -692,6 +723,76 @@ export function HostManagerEditor({
|
||||
domain: "",
|
||||
security: "",
|
||||
ignoreCert: false,
|
||||
guacamoleConfig: {
|
||||
// Display settings
|
||||
colorDepth: undefined,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
dpi: 96,
|
||||
resizeMethod: "display-update",
|
||||
forceLossless: false,
|
||||
// Audio settings
|
||||
disableAudio: false,
|
||||
enableAudioInput: false,
|
||||
// RDP Performance settings
|
||||
enableWallpaper: false,
|
||||
enableTheming: false,
|
||||
enableFontSmoothing: true,
|
||||
enableFullWindowDrag: false,
|
||||
enableDesktopComposition: false,
|
||||
enableMenuAnimations: false,
|
||||
disableBitmapCaching: false,
|
||||
disableOffscreenCaching: false,
|
||||
disableGlyphCaching: false,
|
||||
disableGfx: false,
|
||||
// RDP Device redirection
|
||||
enablePrinting: false,
|
||||
printerName: "",
|
||||
enableDrive: false,
|
||||
driveName: "",
|
||||
drivePath: "",
|
||||
createDrivePath: false,
|
||||
disableDownload: false,
|
||||
disableUpload: false,
|
||||
enableTouch: false,
|
||||
// RDP Session settings
|
||||
clientName: "",
|
||||
console: false,
|
||||
initialProgram: "",
|
||||
serverLayout: "en-us-qwerty",
|
||||
timezone: "",
|
||||
// RDP Gateway settings
|
||||
gatewayHostname: "",
|
||||
gatewayPort: 443,
|
||||
gatewayUsername: "",
|
||||
gatewayPassword: "",
|
||||
gatewayDomain: "",
|
||||
// RDP RemoteApp settings
|
||||
remoteApp: "",
|
||||
remoteAppDir: "",
|
||||
remoteAppArgs: "",
|
||||
// Clipboard settings
|
||||
normalizeClipboard: "preserve",
|
||||
disableCopy: false,
|
||||
disablePaste: false,
|
||||
// VNC specific settings
|
||||
cursor: "remote",
|
||||
swapRedBlue: false,
|
||||
readOnly: false,
|
||||
// Recording settings
|
||||
recordingPath: "",
|
||||
recordingName: "",
|
||||
createRecordingPath: false,
|
||||
recordingExcludeOutput: false,
|
||||
recordingExcludeMouse: false,
|
||||
recordingIncludeKeys: false,
|
||||
// Wake-on-LAN settings
|
||||
wolSendPacket: false,
|
||||
wolMacAddr: "",
|
||||
wolBroadcastAddr: "",
|
||||
wolUdpPort: 9,
|
||||
wolWaitTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -768,6 +869,81 @@ export function HostManagerEditor({
|
||||
console.error("Failed to parse dockerConfig:", error);
|
||||
}
|
||||
|
||||
// Parse guacamoleConfig if it exists - merge with defaults
|
||||
const defaultGuacamoleConfig = {
|
||||
colorDepth: undefined,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
dpi: 96,
|
||||
resizeMethod: "display-update",
|
||||
forceLossless: false,
|
||||
disableAudio: false,
|
||||
enableAudioInput: false,
|
||||
enableWallpaper: false,
|
||||
enableTheming: false,
|
||||
enableFontSmoothing: true,
|
||||
enableFullWindowDrag: false,
|
||||
enableDesktopComposition: false,
|
||||
enableMenuAnimations: false,
|
||||
disableBitmapCaching: false,
|
||||
disableOffscreenCaching: false,
|
||||
disableGlyphCaching: false,
|
||||
disableGfx: false,
|
||||
enablePrinting: false,
|
||||
printerName: "",
|
||||
enableDrive: false,
|
||||
driveName: "",
|
||||
drivePath: "",
|
||||
createDrivePath: false,
|
||||
disableDownload: false,
|
||||
disableUpload: false,
|
||||
enableTouch: false,
|
||||
clientName: "",
|
||||
console: false,
|
||||
initialProgram: "",
|
||||
serverLayout: "en-us-qwerty",
|
||||
timezone: "",
|
||||
gatewayHostname: "",
|
||||
gatewayPort: 443,
|
||||
gatewayUsername: "",
|
||||
gatewayPassword: "",
|
||||
gatewayDomain: "",
|
||||
remoteApp: "",
|
||||
remoteAppDir: "",
|
||||
remoteAppArgs: "",
|
||||
normalizeClipboard: "preserve",
|
||||
disableCopy: false,
|
||||
disablePaste: false,
|
||||
cursor: "remote",
|
||||
swapRedBlue: false,
|
||||
readOnly: false,
|
||||
recordingPath: "",
|
||||
recordingName: "",
|
||||
createRecordingPath: false,
|
||||
recordingExcludeOutput: false,
|
||||
recordingExcludeMouse: false,
|
||||
recordingIncludeKeys: false,
|
||||
wolSendPacket: false,
|
||||
wolMacAddr: "",
|
||||
wolBroadcastAddr: "",
|
||||
wolUdpPort: 9,
|
||||
wolWaitTime: 0,
|
||||
};
|
||||
let parsedGuacamoleConfig = { ...defaultGuacamoleConfig };
|
||||
try {
|
||||
if (cleanedHost.guacamoleConfig) {
|
||||
console.log("[HostManagerEditor] Loading host guacamoleConfig:", cleanedHost.guacamoleConfig);
|
||||
const parsed =
|
||||
typeof cleanedHost.guacamoleConfig === "string"
|
||||
? JSON.parse(cleanedHost.guacamoleConfig)
|
||||
: cleanedHost.guacamoleConfig;
|
||||
parsedGuacamoleConfig = { ...defaultGuacamoleConfig, ...parsed };
|
||||
console.log("[HostManagerEditor] Merged guacamoleConfig:", parsedGuacamoleConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse guacamoleConfig:", error);
|
||||
}
|
||||
|
||||
const formData = {
|
||||
connectionType: (cleanedHost.connectionType || "ssh") as "ssh" | "rdp" | "vnc" | "telnet",
|
||||
name: cleanedHost.name || "",
|
||||
@@ -816,6 +992,8 @@ export function HostManagerEditor({
|
||||
domain: cleanedHost.domain || "",
|
||||
security: cleanedHost.security || "",
|
||||
ignoreCert: Boolean(cleanedHost.ignoreCert),
|
||||
// Guacamole config for RDP/VNC
|
||||
guacamoleConfig: parsedGuacamoleConfig,
|
||||
};
|
||||
|
||||
if (defaultAuthType === "password") {
|
||||
@@ -904,6 +1082,12 @@ export function HostManagerEditor({
|
||||
isSubmittingRef.current = true;
|
||||
setFormError(null);
|
||||
|
||||
// Debug: log form data being submitted
|
||||
console.log("[HostManagerEditor] Form data on submit:", data);
|
||||
console.log("[HostManagerEditor] data.guacamoleConfig:", data.guacamoleConfig);
|
||||
console.log("[HostManagerEditor] data.guacamoleConfig.enableWallpaper:", data.guacamoleConfig?.enableWallpaper);
|
||||
console.log("[HostManagerEditor] form.getValues('guacamoleConfig'):", form.getValues("guacamoleConfig"));
|
||||
|
||||
if (!data.name || data.name.trim() === "") {
|
||||
data.name = `${data.username}@${data.ip}`;
|
||||
}
|
||||
@@ -956,8 +1140,13 @@ export function HostManagerEditor({
|
||||
domain: data.domain || null,
|
||||
security: data.security || null,
|
||||
ignoreCert: Boolean(data.ignoreCert),
|
||||
// Guacamole configuration for RDP/VNC
|
||||
guacamoleConfig: data.guacamoleConfig || null,
|
||||
};
|
||||
|
||||
// Debug: log what we're submitting
|
||||
console.log("[HostManagerEditor] submitData.guacamoleConfig:", submitData.guacamoleConfig);
|
||||
|
||||
submitData.credentialId = null;
|
||||
submitData.password = null;
|
||||
submitData.key = null;
|
||||
@@ -1274,6 +1463,38 @@ export function HostManagerEditor({
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
)}
|
||||
{/* RDP tabs */}
|
||||
{form.watch("connectionType") === "rdp" && (
|
||||
<TabsList>
|
||||
<TabsTrigger value="general">
|
||||
{t("hosts.general")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="display">
|
||||
{t("hosts.display", "Display")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="audio">
|
||||
{t("hosts.audio", "Audio")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="performance">
|
||||
{t("hosts.performance", "Performance")}
|
||||
</TabsTrigger>
|
||||
|
||||
</TabsList>
|
||||
)}
|
||||
{/* VNC tabs */}
|
||||
{form.watch("connectionType") === "vnc" && (
|
||||
<TabsList>
|
||||
<TabsTrigger value="general">
|
||||
{t("hosts.general")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="display">
|
||||
{t("hosts.display", "Display")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="audio">
|
||||
{t("hosts.audio", "Audio")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
)}
|
||||
<TabsContent value="general" className="pt-2">
|
||||
<FormLabel className="mb-3 font-bold">
|
||||
{t("hosts.connectionType", "Connection Type")}
|
||||
@@ -3816,6 +4037,521 @@ export function HostManagerEditor({
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* RDP/VNC Display Tab */}
|
||||
<TabsContent value="display" className="pt-2 space-y-4">
|
||||
<FormLabel className="mb-3 font-bold">
|
||||
{t("hosts.displaySettings", "Display Settings")}
|
||||
</FormLabel>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.width"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("hosts.width", "Width")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Auto"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => field.onChange(e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("hosts.widthDesc", "Display width in pixels (leave empty for auto)")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.height"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("hosts.height", "Height")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Auto"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => field.onChange(e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("hosts.heightDesc", "Display height in pixels (leave empty for auto)")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.dpi"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("hosts.dpi", "DPI")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="96"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => field.onChange(e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("hosts.dpiDesc", "Display resolution in DPI")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.colorDepth"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("hosts.colorDepth", "Color Depth")}</FormLabel>
|
||||
<Select
|
||||
value={field.value?.toString() || "auto"}
|
||||
onValueChange={(value) => field.onChange(value === "auto" ? undefined : parseInt(value))}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("hosts.selectColorDepth", "Select color depth")} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
<SelectItem value="8">8-bit (256 colors)</SelectItem>
|
||||
<SelectItem value="16">16-bit (65536 colors)</SelectItem>
|
||||
<SelectItem value="24">24-bit (True color)</SelectItem>
|
||||
<SelectItem value="32">32-bit (True color + alpha)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{t("hosts.colorDepthDesc", "Color depth for the remote display")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{form.watch("connectionType") === "rdp" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.resizeMethod"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("hosts.resizeMethod", "Resize Method")}</FormLabel>
|
||||
<Select
|
||||
value={field.value || "display-update"}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="display-update">Display Update</SelectItem>
|
||||
<SelectItem value="reconnect">Reconnect</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{t("hosts.resizeMethodDesc", "Method to use when resizing the display")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.forceLossless"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.forceLossless", "Force Lossless")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.forceLosslessDesc", "Force lossless compression (higher bandwidth)")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{form.watch("connectionType") === "vnc" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.cursor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("hosts.cursor", "Cursor Mode")}</FormLabel>
|
||||
<Select
|
||||
value={field.value || "remote"}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="remote">Remote (server-side)</SelectItem>
|
||||
<SelectItem value="local">Local (client-side)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{t("hosts.cursorDesc", "How to render the mouse cursor")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.swapRedBlue"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.swapRedBlue", "Swap Red/Blue")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.swapRedBlueDesc", "Swap red and blue color components")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.readOnly"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.readOnly", "Read Only")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.readOnlyDesc", "View only mode - no input sent to server")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* RDP/VNC Audio Tab */}
|
||||
<TabsContent value="audio" className="pt-2 space-y-4">
|
||||
<FormLabel className="mb-3 font-bold">
|
||||
{t("hosts.audioSettings", "Audio Settings")}
|
||||
</FormLabel>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.disableAudio"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.disableAudio", "Disable Audio")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.disableAudioDesc", "Disable audio playback from the remote session")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{form.watch("connectionType") === "rdp" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.enableAudioInput"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.enableAudioInput", "Enable Audio Input")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.enableAudioInputDesc", "Enable microphone input to the remote session")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* RDP Performance Tab */}
|
||||
<TabsContent value="performance" className="pt-2 space-y-4">
|
||||
<FormLabel className="mb-3 font-bold">
|
||||
{t("hosts.performanceSettings", "Performance Settings")}
|
||||
</FormLabel>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.enableWallpaper"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.enableWallpaper", "Wallpaper")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.enableWallpaperDesc", "Show desktop wallpaper")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={(checked) => {
|
||||
console.log("[HostManagerEditor] Wallpaper toggled to:", checked);
|
||||
field.onChange(checked);
|
||||
// Log the full guacamoleConfig after change
|
||||
setTimeout(() => {
|
||||
console.log("[HostManagerEditor] After toggle, guacamoleConfig:", form.getValues("guacamoleConfig"));
|
||||
}, 0);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.enableTheming"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.enableTheming", "Theming")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.enableThemingDesc", "Enable window theming")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.enableFontSmoothing"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.enableFontSmoothing", "Font Smoothing")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.enableFontSmoothingDesc", "Enable ClearType font smoothing")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value ?? true}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.enableFullWindowDrag"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.enableFullWindowDrag", "Full Window Drag")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.enableFullWindowDragDesc", "Show window contents while dragging")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.enableDesktopComposition"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.enableDesktopComposition", "Desktop Composition")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.enableDesktopCompositionDesc", "Enable Aero glass effects")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.enableMenuAnimations"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.enableMenuAnimations", "Menu Animations")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.enableMenuAnimationsDesc", "Enable menu animations")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormLabel className="mb-3 font-bold pt-4">
|
||||
{t("hosts.cachingSettings", "Caching Settings")}
|
||||
</FormLabel>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.disableBitmapCaching"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.disableBitmapCaching", "Disable Bitmap Caching")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.disableBitmapCachingDesc", "Disable bitmap caching")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.disableOffscreenCaching"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.disableOffscreenCaching", "Disable Offscreen Caching")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.disableOffscreenCachingDesc", "Disable offscreen caching")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.disableGlyphCaching"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.disableGlyphCaching", "Disable Glyph Caching")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.disableGlyphCachingDesc", "Disable glyph caching")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guacamoleConfig.disableGfx"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>{t("hosts.disableGfx", "Disable GFX")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("hosts.disableGfxDesc", "Disable graphics pipeline extension")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -1496,6 +1496,11 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
});
|
||||
} else if (connectionType === "rdp" || connectionType === "vnc") {
|
||||
try {
|
||||
// Parse guacamoleConfig if it's a string
|
||||
const guacConfig = typeof host.guacamoleConfig === "string"
|
||||
? JSON.parse(host.guacamoleConfig)
|
||||
: host.guacamoleConfig;
|
||||
|
||||
const tokenResponse = await getGuacamoleToken({
|
||||
protocol: connectionType,
|
||||
hostname: host.ip,
|
||||
@@ -1505,6 +1510,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
domain: host.domain,
|
||||
security: host.security,
|
||||
ignoreCert: host.ignoreCert,
|
||||
guacamoleConfig: guacConfig,
|
||||
});
|
||||
addTab({
|
||||
type: connectionType,
|
||||
|
||||
@@ -36,30 +36,7 @@ import { Button } from "@/components/ui/button.tsx";
|
||||
import { FolderCard } from "@/ui/desktop/navigation/hosts/FolderCard.tsx";
|
||||
import { getSSHHosts, getSSHFolders } from "@/ui/main-axios.ts";
|
||||
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
||||
import type { SSHFolder } from "@/types/index.ts";
|
||||
|
||||
interface SSHHost {
|
||||
id: number;
|
||||
name: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
username: string;
|
||||
folder: string;
|
||||
tags: string[];
|
||||
pin: boolean;
|
||||
authType: string;
|
||||
password?: string;
|
||||
key?: string;
|
||||
keyPassword?: string;
|
||||
keyType?: string;
|
||||
enableTerminal: boolean;
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: unknown[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
import type { SSHFolder, SSHHost } from "@/types/index.ts";
|
||||
|
||||
interface SidebarProps {
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -115,6 +115,16 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
addTab({ type: "terminal", title, hostConfig: host });
|
||||
} else if (connectionType === "rdp" || connectionType === "vnc") {
|
||||
try {
|
||||
// Parse guacamoleConfig if it's a string
|
||||
const guacConfig = typeof host.guacamoleConfig === "string"
|
||||
? JSON.parse(host.guacamoleConfig)
|
||||
: host.guacamoleConfig;
|
||||
|
||||
// Debug: log what guacamoleConfig we have
|
||||
console.log("[Host.tsx] host.guacamoleConfig type:", typeof host.guacamoleConfig);
|
||||
console.log("[Host.tsx] host.guacamoleConfig:", host.guacamoleConfig);
|
||||
console.log("[Host.tsx] Parsed guacConfig:", guacConfig);
|
||||
|
||||
// Get guacamole token for RDP/VNC connection
|
||||
const tokenResponse = await getGuacamoleToken({
|
||||
protocol: connectionType,
|
||||
@@ -125,6 +135,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
domain: host.domain,
|
||||
security: host.security,
|
||||
ignoreCert: host.ignoreCert,
|
||||
guacamoleConfig: guacConfig,
|
||||
});
|
||||
|
||||
addTab({
|
||||
|
||||
@@ -878,6 +878,8 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
||||
domain: hostData.domain || null,
|
||||
security: hostData.security || null,
|
||||
ignoreCert: Boolean(hostData.ignoreCert),
|
||||
// Guacamole configuration for RDP/VNC
|
||||
guacamoleConfig: hostData.guacamoleConfig || null,
|
||||
};
|
||||
|
||||
if (!submitData.enableTunnel) {
|
||||
@@ -955,6 +957,8 @@ export async function updateSSHHost(
|
||||
domain: hostData.domain || null,
|
||||
security: hostData.security || null,
|
||||
ignoreCert: Boolean(hostData.ignoreCert),
|
||||
// Guacamole configuration for RDP/VNC
|
||||
guacamoleConfig: hostData.guacamoleConfig || null,
|
||||
};
|
||||
|
||||
if (!submitData.enableTunnel) {
|
||||
@@ -3142,16 +3146,171 @@ export interface GuacamoleTokenRequest {
|
||||
domain?: string;
|
||||
security?: string;
|
||||
ignoreCert?: boolean;
|
||||
// Extended guacamole configuration
|
||||
guacamoleConfig?: {
|
||||
// Display settings
|
||||
colorDepth?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
dpi?: number;
|
||||
resizeMethod?: string;
|
||||
forceLossless?: boolean;
|
||||
// Audio settings
|
||||
disableAudio?: boolean;
|
||||
enableAudioInput?: boolean;
|
||||
// RDP Performance settings
|
||||
enableWallpaper?: boolean;
|
||||
enableTheming?: boolean;
|
||||
enableFontSmoothing?: boolean;
|
||||
enableFullWindowDrag?: boolean;
|
||||
enableDesktopComposition?: boolean;
|
||||
enableMenuAnimations?: boolean;
|
||||
disableBitmapCaching?: boolean;
|
||||
disableOffscreenCaching?: boolean;
|
||||
disableGlyphCaching?: boolean;
|
||||
disableGfx?: boolean;
|
||||
// RDP Device redirection
|
||||
enablePrinting?: boolean;
|
||||
printerName?: string;
|
||||
enableDrive?: boolean;
|
||||
driveName?: string;
|
||||
drivePath?: string;
|
||||
createDrivePath?: boolean;
|
||||
disableDownload?: boolean;
|
||||
disableUpload?: boolean;
|
||||
enableTouch?: boolean;
|
||||
// RDP Session settings
|
||||
clientName?: string;
|
||||
console?: boolean;
|
||||
initialProgram?: string;
|
||||
serverLayout?: string;
|
||||
timezone?: string;
|
||||
// RDP Gateway settings
|
||||
gatewayHostname?: string;
|
||||
gatewayPort?: number;
|
||||
gatewayUsername?: string;
|
||||
gatewayPassword?: string;
|
||||
gatewayDomain?: string;
|
||||
// RDP RemoteApp settings
|
||||
remoteApp?: string;
|
||||
remoteAppDir?: string;
|
||||
remoteAppArgs?: string;
|
||||
// Clipboard settings
|
||||
normalizeClipboard?: string;
|
||||
disableCopy?: boolean;
|
||||
disablePaste?: boolean;
|
||||
// VNC specific settings
|
||||
cursor?: string;
|
||||
swapRedBlue?: boolean;
|
||||
readOnly?: boolean;
|
||||
// Recording settings
|
||||
recordingPath?: string;
|
||||
recordingName?: string;
|
||||
createRecordingPath?: boolean;
|
||||
recordingExcludeOutput?: boolean;
|
||||
recordingExcludeMouse?: boolean;
|
||||
recordingIncludeKeys?: boolean;
|
||||
// Wake-on-LAN settings
|
||||
wolSendPacket?: boolean;
|
||||
wolMacAddr?: string;
|
||||
wolBroadcastAddr?: string;
|
||||
wolUdpPort?: number;
|
||||
wolWaitTime?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuacamoleTokenResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
// Helper to convert camelCase to kebab-case for guacamole parameters
|
||||
function toGuacamoleParams(config: GuacamoleTokenRequest["guacamoleConfig"]): Record<string, unknown> {
|
||||
if (!config) return {};
|
||||
|
||||
const params: Record<string, unknown> = {};
|
||||
|
||||
// Map camelCase to guacamole's kebab-case parameter names
|
||||
const mappings: Record<string, string> = {
|
||||
colorDepth: "color-depth",
|
||||
resizeMethod: "resize-method",
|
||||
forceLossless: "force-lossless",
|
||||
disableAudio: "disable-audio",
|
||||
enableAudioInput: "enable-audio-input",
|
||||
enableWallpaper: "enable-wallpaper",
|
||||
enableTheming: "enable-theming",
|
||||
enableFontSmoothing: "enable-font-smoothing",
|
||||
enableFullWindowDrag: "enable-full-window-drag",
|
||||
enableDesktopComposition: "enable-desktop-composition",
|
||||
enableMenuAnimations: "enable-menu-animations",
|
||||
disableBitmapCaching: "disable-bitmap-caching",
|
||||
disableOffscreenCaching: "disable-offscreen-caching",
|
||||
disableGlyphCaching: "disable-glyph-caching",
|
||||
disableGfx: "disable-gfx",
|
||||
enablePrinting: "enable-printing",
|
||||
printerName: "printer-name",
|
||||
enableDrive: "enable-drive",
|
||||
driveName: "drive-name",
|
||||
drivePath: "drive-path",
|
||||
createDrivePath: "create-drive-path",
|
||||
disableDownload: "disable-download",
|
||||
disableUpload: "disable-upload",
|
||||
enableTouch: "enable-touch",
|
||||
clientName: "client-name",
|
||||
initialProgram: "initial-program",
|
||||
serverLayout: "server-layout",
|
||||
gatewayHostname: "gateway-hostname",
|
||||
gatewayPort: "gateway-port",
|
||||
gatewayUsername: "gateway-username",
|
||||
gatewayPassword: "gateway-password",
|
||||
gatewayDomain: "gateway-domain",
|
||||
remoteApp: "remote-app",
|
||||
remoteAppDir: "remote-app-dir",
|
||||
remoteAppArgs: "remote-app-args",
|
||||
normalizeClipboard: "normalize-clipboard",
|
||||
disableCopy: "disable-copy",
|
||||
disablePaste: "disable-paste",
|
||||
swapRedBlue: "swap-red-blue",
|
||||
readOnly: "read-only",
|
||||
recordingPath: "recording-path",
|
||||
recordingName: "recording-name",
|
||||
createRecordingPath: "create-recording-path",
|
||||
recordingExcludeOutput: "recording-exclude-output",
|
||||
recordingExcludeMouse: "recording-exclude-mouse",
|
||||
recordingIncludeKeys: "recording-include-keys",
|
||||
wolSendPacket: "wol-send-packet",
|
||||
wolMacAddr: "wol-mac-addr",
|
||||
wolBroadcastAddr: "wol-broadcast-addr",
|
||||
wolUdpPort: "wol-udp-port",
|
||||
wolWaitTime: "wol-wait-time",
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
const paramName = mappings[key] || key;
|
||||
// Guacamole expects boolean values as strings "true" or "false"
|
||||
if (typeof value === "boolean") {
|
||||
params[paramName] = value ? "true" : "false";
|
||||
} else {
|
||||
params[paramName] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export async function getGuacamoleToken(
|
||||
request: GuacamoleTokenRequest,
|
||||
): Promise<GuacamoleTokenResponse> {
|
||||
try {
|
||||
// Convert guacamoleConfig to guacamole parameter format
|
||||
const guacParams = toGuacamoleParams(request.guacamoleConfig);
|
||||
|
||||
// Debug: log guacamoleConfig and converted params
|
||||
console.log("[Guacamole] Request guacamoleConfig:", request.guacamoleConfig);
|
||||
console.log("[Guacamole] Converted params:", guacParams);
|
||||
console.log("[Guacamole] Param count:", Object.keys(guacParams).length);
|
||||
|
||||
// Use authApi (port 30001 without /ssh prefix) since guacamole routes are at /guacamole
|
||||
const response = await authApi.post("/guacamole/token", {
|
||||
type: request.protocol,
|
||||
@@ -3162,6 +3321,7 @@ export async function getGuacamoleToken(
|
||||
domain: request.domain,
|
||||
security: request.security,
|
||||
"ignore-cert": request.ignoreCert,
|
||||
...guacParams,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user