feat: finish the remote-desktop plugin (B14)

This commit is contained in:
LukeGus committed 2026-09-24 01:02:45 -05:00
1 parent 0af2c94983
commit 14eb558b85
215 files changed
+30029 -3921

No files matched your search

+2 -8
View File
@@ -819,9 +819,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
socks5_password TEXT,
socks5_proxy_chain TEXT,
domain TEXT,
security TEXT,
ignore_cert INTEGER NOT NULL DEFAULT 0,
guacamole_config TEXT,
mac_address TEXT,
port_knock_sequence TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -931,8 +928,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
const sshHosts =
await createCurrentHostRepository().listDecryptedByUserId(userId);
const insertHost = exportDb.prepare(`
INSERT INTO ssh_data (id, user_id, connection_type, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, enable_web_ui, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, docker_config, web_ui_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, domain, security, ignore_cert, guacamole_config, mac_address, port_knock_sequence, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO ssh_data (id, user_id, connection_type, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, enable_web_ui, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, docker_config, web_ui_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, domain, mac_address, port_knock_sequence, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const decrypted of sshHosts) {
@@ -985,9 +982,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.socks5Password || null,
decrypted.socks5ProxyChain || null,
decrypted.domain || null,
decrypted.security || null,
decrypted.ignoreCert ? 1 : 0,
decrypted.guacamoleConfig || null,
decrypted.macAddress || null,
decrypted.portKnockSequence || null,
decrypted.createdAt,
-34
View File
@@ -19,7 +19,6 @@ import {
assertDataDirIsNotMisconfigured,
DataDirMisconfiguredError,
} from "../../utils/data-dir-guard.js";
import { getDefaultGuacdUrl } from "../../utils/guacd-config.js";
import { resolveDatabaseDialect, type DatabaseDialect } from "./dialect.js";
import { SYSTEM_ROLE_DEFAULTS } from "../../utils/permission-catalog.js";
import { connectRemoteDatabase } from "./connect.js";
@@ -807,24 +806,6 @@ async function initializeCompleteDatabase(): Promise<void> {
error: e,
});
}
try {
ensureRawSettingDefault("guac_enabled", "true");
} catch (e) {
databaseLogger.warn("Could not initialize guac_enabled setting", {
operation: "db_init",
error: e,
});
}
try {
ensureRawSettingDefault("guac_url", getDefaultGuacdUrl());
} catch (e) {
databaseLogger.warn("Could not initialize guac_url setting", {
operation: "db_init",
error: e,
});
}
}
const addColumnIfNotExists = (
@@ -948,7 +929,6 @@ const migrateSchema = () => {
addColumnIfNotExists("user_preferences", "custom_themes", "TEXT");
addColumnIfNotExists("user_preferences", "custom_keybindings", "TEXT");
addColumnIfNotExists("user_preferences", "terminal_defaults", "TEXT");
addColumnIfNotExists("user_preferences", "rdp_defaults", "TEXT");
addColumnIfNotExists("user_preferences", "terminal_macros", "TEXT");
sqlite.exec(`
@@ -1164,9 +1144,6 @@ const migrateSchema = () => {
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_data", "guacamole_config", "TEXT");
addColumnIfNotExists("ssh_data", "notes", "TEXT");
addColumnIfNotExists("ssh_data", "use_socks5", "INTEGER");
@@ -1554,9 +1531,6 @@ const migrateSchema = () => {
{ column: "show_server_stats_in_sidebar", sql: "ALTER TABLE ssh_data ADD COLUMN show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0" },
{ column: "quick_actions", sql: "ALTER TABLE ssh_data ADD COLUMN quick_actions TEXT" },
{ column: "domain", sql: "ALTER TABLE ssh_data ADD COLUMN domain TEXT" },
{ column: "security", sql: "ALTER TABLE ssh_data ADD COLUMN security TEXT" },
{ column: "ignore_cert", sql: "ALTER TABLE ssh_data ADD COLUMN ignore_cert INTEGER NOT NULL DEFAULT 0" },
{ column: "guacamole_config", sql: "ALTER TABLE ssh_data ADD COLUMN guacamole_config TEXT" },
{ column: "socks5_proxy_chain", sql: "ALTER TABLE ssh_data ADD COLUMN socks5_proxy_chain TEXT" },
{ column: "host_key_fingerprint", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_fingerprint TEXT" },
{ column: "host_key_type", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_type TEXT" },
@@ -1567,18 +1541,10 @@ const migrateSchema = () => {
{ column: "mac_address", sql: "ALTER TABLE ssh_data ADD COLUMN mac_address TEXT" },
{ column: "port_knock_sequence", sql: "ALTER TABLE ssh_data ADD COLUMN port_knock_sequence TEXT" },
{ column: "enable_ssh", sql: "ALTER TABLE ssh_data ADD COLUMN enable_ssh INTEGER NOT NULL DEFAULT 1" },
{ column: "enable_rdp", sql: "ALTER TABLE ssh_data ADD COLUMN enable_rdp INTEGER NOT NULL DEFAULT 0" },
{ column: "enable_vnc", sql: "ALTER TABLE ssh_data ADD COLUMN enable_vnc INTEGER NOT NULL DEFAULT 0" },
{ column: "enable_telnet", sql: "ALTER TABLE ssh_data ADD COLUMN enable_telnet INTEGER NOT NULL DEFAULT 0" },
{ column: "ssh_port", sql: "ALTER TABLE ssh_data ADD COLUMN ssh_port INTEGER DEFAULT 22" },
{ column: "rdp_port", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_port INTEGER DEFAULT 3389" },
{ column: "vnc_port", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_port INTEGER DEFAULT 5900" },
{ column: "telnet_port", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_port INTEGER DEFAULT 23" },
{ column: "rdp_user", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_user TEXT" },
{ column: "rdp_password", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_password TEXT" },
{ column: "rdp_domain", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_domain TEXT" },
{ column: "rdp_security", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_security TEXT" },
{ column: "rdp_ignore_cert", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_ignore_cert INTEGER DEFAULT 0" },
{ column: "vnc_password", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_password TEXT" },
{ column: "vnc_user", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_user TEXT" },
{ column: "telnet_user", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_user TEXT" },
-12
View File
@@ -302,21 +302,13 @@ export const hosts = mysqlTable(
quickActions: text("quick_actions"),
notes: text("notes"),
enableSsh: boolean("enable_ssh").notNull().default(true),
enableRdp: boolean("enable_rdp").notNull().default(false),
enableVnc: boolean("enable_vnc").notNull().default(false),
enableTelnet: boolean("enable_telnet").notNull().default(false),
sshPort: int("ssh_port").default(22),
rdpPort: int("rdp_port").default(3389),
vncPort: int("vnc_port").default(5900),
telnetPort: int("telnet_port").default(23),
rdpCredentialId: int("rdp_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }),
rdpUser: text("rdp_user"),
rdpPassword: text("rdp_password"),
rdpDomain: text("rdp_domain"),
rdpSecurity: text("rdp_security"),
rdpIgnoreCert: boolean("rdp_ignore_cert").default(false),
vncCredentialId: int("vnc_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }),
vncPassword: text("vnc_password"),
@@ -331,9 +323,6 @@ export const hosts = mysqlTable(
telnetAuthType: text("telnet_auth_type"),
domain: text("domain"),
security: text("security"),
ignoreCert: boolean("ignore_cert").default(false),
guacamoleConfig: text("guacamole_config"),
useSocks5: boolean("use_socks5"),
socks5Host: text("socks5_host"),
@@ -892,7 +881,6 @@ export const userPreferences = mysqlTable("user_preferences", {
customThemes: text("custom_themes"),
customKeybindings: text("custom_keybindings"),
terminalDefaults: text("terminal_defaults"),
rdpDefaults: text("rdp_defaults"),
terminalMacros: text("terminal_macros"),
updatedAt: varchar("updated_at", { length: 255 })
.notNull()
-12
View File
@@ -303,21 +303,13 @@ export const hosts = pgTable(
quickActions: text("quick_actions"),
notes: text("notes"),
enableSsh: boolean("enable_ssh").notNull().default(true),
enableRdp: boolean("enable_rdp").notNull().default(false),
enableVnc: boolean("enable_vnc").notNull().default(false),
enableTelnet: boolean("enable_telnet").notNull().default(false),
sshPort: integer("ssh_port").default(22),
rdpPort: integer("rdp_port").default(3389),
vncPort: integer("vnc_port").default(5900),
telnetPort: integer("telnet_port").default(23),
rdpCredentialId: integer("rdp_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }),
rdpUser: text("rdp_user"),
rdpPassword: text("rdp_password"),
rdpDomain: text("rdp_domain"),
rdpSecurity: text("rdp_security"),
rdpIgnoreCert: boolean("rdp_ignore_cert").default(false),
vncCredentialId: integer("vnc_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }),
vncPassword: text("vnc_password"),
@@ -332,9 +324,6 @@ export const hosts = pgTable(
telnetAuthType: text("telnet_auth_type"),
domain: text("domain"),
security: text("security"),
ignoreCert: boolean("ignore_cert").default(false),
guacamoleConfig: text("guacamole_config"),
useSocks5: boolean("use_socks5"),
socks5Host: text("socks5_host"),
@@ -893,7 +882,6 @@ export const userPreferences = pgTable("user_preferences", {
customThemes: text("custom_themes"),
customKeybindings: text("custom_keybindings"),
terminalDefaults: text("terminal_defaults"),
rdpDefaults: text("rdp_defaults"),
terminalMacros: text("terminal_macros"),
updatedAt: varchar("updated_at", { length: 255 })
.notNull()
-12
View File
@@ -295,21 +295,13 @@ export const hosts = sqliteTable(
quickActions: text("quick_actions"),
notes: text("notes"),
enableSsh: integer("enable_ssh", { mode: "boolean" }).notNull().default(true),
enableRdp: integer("enable_rdp", { mode: "boolean" }).notNull().default(false),
enableVnc: integer("enable_vnc", { mode: "boolean" }).notNull().default(false),
enableTelnet: integer("enable_telnet", { mode: "boolean" }).notNull().default(false),
sshPort: integer("ssh_port").default(22),
rdpPort: integer("rdp_port").default(3389),
vncPort: integer("vnc_port").default(5900),
telnetPort: integer("telnet_port").default(23),
rdpCredentialId: integer("rdp_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }),
rdpUser: text("rdp_user"),
rdpPassword: text("rdp_password"),
rdpDomain: text("rdp_domain"),
rdpSecurity: text("rdp_security"),
rdpIgnoreCert: integer("rdp_ignore_cert", { mode: "boolean" }).default(false),
vncCredentialId: integer("vnc_credential_id").references(() => sshCredentials.id, { onDelete: "set null" }),
vncPassword: text("vnc_password"),
@@ -324,9 +316,6 @@ export const hosts = sqliteTable(
telnetAuthType: text("telnet_auth_type"),
domain: text("domain"),
security: text("security"),
ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false),
guacamoleConfig: text("guacamole_config"),
useSocks5: integer("use_socks5", { mode: "boolean" }),
socks5Host: text("socks5_host"),
@@ -889,7 +878,6 @@ export const userPreferences = sqliteTable("user_preferences", {
customThemes: text("custom_themes"),
customKeybindings: text("custom_keybindings"),
terminalDefaults: text("terminal_defaults"),
rdpDefaults: text("rdp_defaults"),
terminalMacros: text("terminal_macros"),
updatedAt: text("updated_at")
.notNull()
@@ -812,9 +812,6 @@ export function registerHostBulkRoutes(
? 1
: 0,
enableSsh: hostData.enableSsh ?? effectiveConnectionType === "ssh",
enableRdp: hostData.enableRdp ?? false,
enableVnc: hostData.enableVnc ?? false,
enableTelnet: hostData.enableTelnet ?? false,
updatedAt: new Date().toISOString(),
};
@@ -828,21 +825,10 @@ export function registerHostBulkRoutes(
sshDataObj.rdpUser = hostData.rdpUser || null;
sshDataObj.rdpPassword = hostData.rdpPassword || null;
sshDataObj.rdpDomain = hostData.rdpDomain || null;
sshDataObj.rdpSecurity = hostData.rdpSecurity || null;
sshDataObj.rdpIgnoreCert = hostData.rdpIgnoreCert ? 1 : 0;
sshDataObj.rdpPort = hostData.rdpPort || 3389;
sshDataObj.vncUser = hostData.vncUser || null;
sshDataObj.vncPassword = hostData.vncPassword || null;
sshDataObj.vncPort = hostData.vncPort || 5900;
sshDataObj.telnetUser = hostData.telnetUser || null;
sshDataObj.telnetPassword = hostData.telnetPassword || null;
sshDataObj.telnetPort = hostData.telnetPort || 23;
sshDataObj.enableRdp = hostData.enableRdp ? 1 : 0;
sshDataObj.enableVnc = hostData.enableVnc ? 1 : 0;
sshDataObj.enableTelnet = hostData.enableTelnet ? 1 : 0;
sshDataObj.guacamoleConfig = hostData.guacamoleConfig
? JSON.stringify(hostData.guacamoleConfig)
: null;
} else {
sshDataObj.password =
hostData.authType === "password" ? hostData.password : null;
@@ -855,9 +841,6 @@ export function registerHostBulkRoutes(
sshDataObj.keyType =
hostData.authType === "key" ? hostData.keyType || "auto" : null;
sshDataObj.domain = null;
sshDataObj.security = null;
sshDataObj.ignoreCert = 0;
sshDataObj.guacamoleConfig = null;
}
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
@@ -1079,9 +1062,6 @@ export function registerHostBulkRoutes(
portKnockSequence: null,
overrideCredentialUsername: 0,
enableSsh: true,
enableRdp: false,
enableVnc: false,
enableTelnet: false,
updatedAt: new Date().toISOString(),
};
@@ -23,12 +23,7 @@ export function applyHostKeyTypeUpdate(
}
}
const PROTOCOL_ENABLE_FIELDS = [
"enableSsh",
"enableRdp",
"enableVnc",
"enableTelnet",
] as const;
const PROTOCOL_ENABLE_FIELDS = ["enableSsh"] as const;
export function normalizeProtocolEnableFields(
values: Record<string, unknown>,
@@ -214,13 +209,7 @@ export type NormalizedImportedHost = Record<string, unknown> & {
portKnockSequence?: unknown;
overrideCredentialUsername?: unknown;
domain?: unknown;
security?: unknown;
ignoreCert?: unknown;
guacamoleConfig?: unknown;
enableSsh: boolean;
enableRdp: boolean;
enableVnc: boolean;
enableTelnet: boolean;
};
export function normalizeImportedHost(
@@ -275,18 +264,6 @@ export function normalizeImportedHost(
hostData.enableSsh === undefined
? connectionType === "ssh"
: asBoolean(hostData.enableSsh),
enableRdp:
hostData.enableRdp === undefined
? connectionType === "rdp"
: asBoolean(hostData.enableRdp),
enableVnc:
hostData.enableVnc === undefined
? connectionType === "vnc"
: asBoolean(hostData.enableVnc),
enableTelnet:
hostData.enableTelnet === undefined
? connectionType === "telnet"
: asBoolean(hostData.enableTelnet),
};
}
@@ -372,14 +349,8 @@ const CONNECT_LEVEL_FIELDS = new Set([
"showDockerInSidebar",
"showServerStatsInSidebar",
"enableSsh",
"enableRdp",
"enableVnc",
"enableTelnet",
"sshPort",
"rdpPort",
"rdpAuthType",
"vncPort",
"telnetPort",
"defaultPath",
"scpLegacy",
"tunnelConnections",
@@ -472,31 +443,10 @@ export function transformHostResponse(
showTunnelInSidebar: !!host.showTunnelInSidebar,
showDockerInSidebar: !!host.showDockerInSidebar,
showServerStatsInSidebar: !!host.showServerStatsInSidebar,
// Old hosts only had connection_type set; the per-protocol enable flags didn't exist yet.
// The schema defaults (enableSsh=true, others=false) wrongly mark every old host as SSH.
// Detect this migration case: if no non-SSH protocol is explicitly enabled AND
// connectionType is set to a non-SSH value, fall back to inferring from connectionType.
...(() => {
const ct = host.connectionType;
const rdp = !!host.enableRdp;
const vnc = !!host.enableVnc;
const tel = !!host.enableTelnet;
const isMigratedNonSsh = !rdp && !vnc && !tel && ct && ct !== "ssh";
return {
enableSsh: isMigratedNonSsh ? false : !!host.enableSsh,
enableRdp: isMigratedNonSsh ? ct === "rdp" : rdp,
enableVnc: isMigratedNonSsh ? ct === "vnc" : vnc,
enableTelnet: isMigratedNonSsh ? ct === "telnet" : tel,
};
})(),
enableSsh: !!host.enableSsh,
sshPort: host.sshPort ?? host.port ?? 22,
rdpPort: host.rdpPort ?? 3389,
vncPort: host.vncPort ?? 5900,
telnetPort: host.telnetPort ?? 23,
rdpUser: host.rdpUser || undefined,
rdpDomain: host.rdpDomain || undefined,
rdpSecurity: host.rdpSecurity || undefined,
rdpIgnoreCert: !!host.rdpIgnoreCert,
vncUser: host.vncUser || undefined,
telnetUser: host.telnetUser || undefined,
tunnelConnections: host.tunnelConnections
@@ -524,10 +474,5 @@ export function transformHostResponse(
? JSON.parse(host.portKnockSequence as string)
: [],
domain: host.domain || undefined,
security: host.security || undefined,
ignoreCert: !!host.ignoreCert,
guacamoleConfig: host.guacamoleConfig
? JSON.parse(host.guacamoleConfig as string)
: undefined,
};
}
+22 -78
View File
@@ -68,7 +68,6 @@ import type {
HostResolutionCredentialRecord,
HostResolutionHostRecord,
} from "../repositories/host-resolution-repository.js";
import { AUTH_PROTOCOL_METADATA } from "../../../types/auth-protocols.js";
import {
requiresPersonalHostAuthentication,
resolveRecipientSharedHostAuthentication,
@@ -216,9 +215,6 @@ router.post(
terminalConfig,
forceKeyboardInteractive,
domain,
security,
ignoreCert,
guacamoleConfig,
notes,
useSocks5,
socks5Host,
@@ -232,20 +228,12 @@ router.post(
macAddress,
wolBroadcastAddress,
enableSsh,
enableRdp,
enableVnc,
enableTelnet,
sshPort,
rdpPort,
vncPort,
telnetPort,
rdpAuthType,
rdpCredentialId,
rdpUser,
rdpPassword,
rdpDomain,
rdpSecurity,
rdpIgnoreCert,
vncAuthType,
vncCredentialId,
vncPassword,
@@ -267,7 +255,7 @@ router.post(
!isNonEmptyString(ip) ||
!isValidPort(port) ||
!isOptionalBoolean(shareSshAuth) ||
![enableSsh, enableRdp, enableVnc, enableTelnet].every(isOptionalBoolean)
!isOptionalBoolean(enableSsh)
) {
sshLogger.warn("Invalid SSH data input validation failed", {
operation: "host_create",
@@ -364,9 +352,6 @@ router.post(
: null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
domain: domain || null,
security: security || null,
ignoreCert: ignoreCert ? 1 : 0,
guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
notes: notes || null,
sudoPassword: sudoPassword || null,
useSocks5: useSocks5 ? 1 : 0,
@@ -388,27 +373,22 @@ router.post(
: null,
...normalizeProtocolEnableFields(hostData),
sshPort: sshPort || port || 22,
rdpPort: rdpPort || 3389,
vncPort: vncPort || 5900,
telnetPort: telnetPort || 23,
rdpAuthType: enableRdp ? rdpAuthType || null : null,
rdpAuthType: rdpAuthType || null,
rdpCredentialId:
enableRdp && rdpAuthType === "credential" && rdpCredentialId
rdpAuthType === "credential" && rdpCredentialId
? rdpCredentialId
: null,
rdpUser: rdpUser || null,
rdpDomain: rdpDomain || null,
rdpSecurity: rdpSecurity || null,
rdpIgnoreCert: rdpIgnoreCert ? 1 : 0,
vncAuthType: enableVnc ? vncAuthType || null : null,
vncAuthType: vncAuthType || null,
vncCredentialId:
enableVnc && vncAuthType === "credential" && vncCredentialId
vncAuthType === "credential" && vncCredentialId
? vncCredentialId
: null,
vncUser: vncUser || null,
telnetAuthType: enableTelnet ? telnetAuthType || null : null,
telnetAuthType: telnetAuthType || null,
telnetCredentialId:
enableTelnet && telnetAuthType === "credential" && telnetCredentialId
telnetAuthType === "credential" && telnetCredentialId
? telnetCredentialId
: null,
telnetUser: telnetUser || null,
@@ -932,9 +912,6 @@ router.put(
terminalConfig,
forceKeyboardInteractive,
domain,
security,
ignoreCert,
guacamoleConfig,
notes,
useSocks5,
socks5Host,
@@ -948,20 +925,12 @@ router.put(
macAddress,
wolBroadcastAddress,
enableSsh,
enableRdp,
enableVnc,
enableTelnet,
sshPort,
rdpPort,
vncPort,
telnetPort,
rdpAuthType,
rdpCredentialId,
rdpUser,
rdpPassword,
rdpDomain,
rdpSecurity,
rdpIgnoreCert,
vncAuthType,
vncCredentialId,
vncPassword,
@@ -983,9 +952,7 @@ router.put(
!isNonEmptyString(ip) ||
!isValidPort(port) ||
!isOptionalBoolean(shareSshAuth) ||
![enableSsh, enableRdp, enableVnc, enableTelnet].every(
isOptionalBoolean,
) ||
!isOptionalBoolean(enableSsh) ||
!hostId
) {
sshLogger.warn("Invalid SSH data input validation failed for update", {
@@ -1083,9 +1050,6 @@ router.put(
: null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
domain: domain || null,
security: security || null,
ignoreCert: ignoreCert ? 1 : 0,
guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
notes: notes || null,
sudoPassword: sudoPassword || null,
useSocks5: useSocks5 ? 1 : 0,
@@ -1107,27 +1071,22 @@ router.put(
: null,
...normalizeProtocolEnableFields(hostData),
sshPort: sshPort || port || 22,
rdpPort: rdpPort || 3389,
vncPort: vncPort || 5900,
telnetPort: telnetPort || 23,
rdpAuthType: enableRdp ? rdpAuthType || null : null,
rdpAuthType: rdpAuthType || null,
rdpCredentialId:
enableRdp && rdpAuthType === "credential" && rdpCredentialId
rdpAuthType === "credential" && rdpCredentialId
? rdpCredentialId
: null,
rdpUser: rdpUser || null,
rdpDomain: rdpDomain || null,
rdpSecurity: rdpSecurity || null,
rdpIgnoreCert: rdpIgnoreCert ? 1 : 0,
vncAuthType: enableVnc ? vncAuthType || null : null,
vncAuthType: vncAuthType || null,
vncCredentialId:
enableVnc && vncAuthType === "credential" && vncCredentialId
vncAuthType === "credential" && vncCredentialId
? vncCredentialId
: null,
vncUser: vncUser || null,
telnetAuthType: enableTelnet ? telnetAuthType || null : null,
telnetAuthType: telnetAuthType || null,
telnetCredentialId:
enableTelnet && telnetAuthType === "credential" && telnetCredentialId
telnetAuthType === "credential" && telnetCredentialId
? telnetCredentialId
: null,
telnetUser: telnetUser || null,
@@ -2085,9 +2044,7 @@ router.get(
const exportedConnectionType =
(resolvedHost.connectionType as string) || "ssh";
const isRemoteDesktop = ["rdp", "vnc", "telnet"].includes(
exportedConnectionType,
);
const isRemoteDesktop = exportedConnectionType !== "ssh";
const baseExportData = {
exportId: resolvedHost.id,
@@ -2104,24 +2061,18 @@ router.get(
: resolvedHost.tags || [],
pin: !!resolvedHost.pin,
notes: resolvedHost.notes || null,
// Every plugin's host settings, secrets redacted, for import to hand back.
pluginSettings: hostPluginSettings ?? {},
};
const exportData = isRemoteDesktop
? {
...baseExportData,
enableRdp: !!resolvedHost.enableRdp,
enableVnc: !!resolvedHost.enableVnc,
enableTelnet: !!resolvedHost.enableTelnet,
rdpPort: resolvedHost.rdpPort || 3389,
vncPort: resolvedHost.vncPort || 5900,
telnetPort: resolvedHost.telnetPort || 23,
rdpAuthType: resolvedHost.rdpAuthType || null,
rdpCredentialId: resolvedHost.rdpCredentialId || null,
rdpUser: resolvedHost.rdpUser || null,
rdpPassword: resolvedHost.rdpPassword || null,
rdpDomain: resolvedHost.rdpDomain || null,
rdpSecurity: resolvedHost.rdpSecurity || null,
rdpIgnoreCert: !!resolvedHost.rdpIgnoreCert,
vncAuthType: resolvedHost.vncAuthType || null,
vncCredentialId: resolvedHost.vncCredentialId || null,
vncUser: resolvedHost.vncUser || null,
@@ -2130,9 +2081,6 @@ router.get(
telnetCredentialId: resolvedHost.telnetCredentialId || null,
telnetUser: resolvedHost.telnetUser || null,
telnetPassword: resolvedHost.telnetPassword || null,
guacamoleConfig: resolvedHost.guacamoleConfig
? JSON.parse(resolvedHost.guacamoleConfig as string)
: null,
}
: {
...baseExportData,
@@ -2273,9 +2221,7 @@ router.get(
const exportedConnectionType =
(resolvedHost.connectionType as string) || "ssh";
const isRemoteDesktop = ["rdp", "vnc", "telnet"].includes(
exportedConnectionType,
);
const isRemoteDesktop = exportedConnectionType !== "ssh";
const baseExportData = {
exportId: resolvedHost.id,
@@ -2292,17 +2238,14 @@ router.get(
: resolvedHost.tags || [],
pin: !!resolvedHost.pin,
notes: resolvedHost.notes || null,
// Every plugin's host settings, secrets redacted, for import to hand back.
pluginSettings: hostPluginSettings ?? {},
};
const exportData = isRemoteDesktop
? {
...baseExportData,
domain: resolvedHost.domain || null,
security: resolvedHost.security || null,
ignoreCert: !!resolvedHost.ignoreCert,
guacamoleConfig: resolvedHost.guacamoleConfig
? JSON.parse(resolvedHost.guacamoleConfig as string)
: null,
}
: {
...baseExportData,
@@ -2638,8 +2581,9 @@ async function resolveHostCredentials(
host.id,
requestingUserId,
);
// Whether a protocol is on is the remote-desktop plugin's host
// setting; the client only offers an override for one that is.
for (const protocol of ["rdp", "vnc", "telnet"] as const) {
if (!host[AUTH_PROTOCOL_METADATA[protocol].enableField]) continue;
authOverrides[protocol] = {
credentialId: overrideCredentialIds[protocol],
required: false,
@@ -39,11 +39,10 @@ const pickPreferences = (row?: UserPreferenceRecord | null) => ({
customThemes: row?.customThemes ?? null,
customKeybindings: row?.customKeybindings ?? null,
terminalDefaults: row?.terminalDefaults ?? null,
rdpDefaults: row?.rdpDefaults ?? null,
terminalMacros: row?.terminalMacros ?? null,
});
const connectionDefaultFields = ["terminalDefaults", "rdpDefaults"] as const;
const connectionDefaultFields = ["terminalDefaults"] as const;
export function validateDefaultsJson(value: string): boolean {
if (value.length > 32_768) return false;
@@ -233,7 +232,6 @@ router.put("/", authenticateJWT, async (req: Request, res: Response) => {
customThemes,
customKeybindings,
terminalDefaults,
rdpDefaults,
terminalMacros,
} = req.body as {
reopenTabsOnLogin?: boolean;
@@ -256,7 +254,6 @@ router.put("/", authenticateJWT, async (req: Request, res: Response) => {
customThemes?: string | null;
customKeybindings?: string | null;
terminalDefaults?: string | null;
rdpDefaults?: string | null;
terminalMacros?: string | null;
};
// showHostTags, hostTrayOnClick, compactHostView, statusColorScheme are no
@@ -289,7 +286,6 @@ router.put("/", authenticateJWT, async (req: Request, res: Response) => {
customThemes,
customKeybindings,
terminalDefaults,
rdpDefaults,
terminalMacros,
})) {
if (value !== undefined && value !== null && typeof value !== "string") {
@@ -299,7 +295,6 @@ router.put("/", authenticateJWT, async (req: Request, res: Response) => {
const connectionDefaults = {
terminalDefaults,
rdpDefaults,
};
for (const key of connectionDefaultFields) {
const value = connectionDefaults[key];
@@ -435,7 +430,6 @@ router.put("/", authenticateJWT, async (req: Request, res: Response) => {
updates.customKeybindings = customKeybindings;
if (terminalDefaults !== undefined)
updates.terminalDefaults = terminalDefaults;
if (rdpDefaults !== undefined) updates.rdpDefaults = rdpDefaults;
if (terminalMacros !== undefined) updates.terminalMacros = terminalMacros;
if (Object.keys(updates).length === 1) {
@@ -1,12 +1,6 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import { getErrorMessage } from "../../utils/error-message.js";
import type { RequestHandler, Router } from "express";
// Remote Desktop is a first-party plugin (plugins/remote-desktop); this goes
// through the same in-core bridge its router is mounted behind, rather than
// importing the plugin's TypeScript directly, since plugins/ is compiled
// separately from src/backend/ and core cannot statically import across that
// boundary. See guacamole-dispatch.ts and that plugin's README.
import { restartGuacamoleService as restartGuacServer } from "../../hosts/guacamole-sessions.js";
import {
authLogger,
getGlobalLogLevel,
@@ -31,10 +25,6 @@ import {
} from "../repositories/factory.js";
import type { UserRecord } from "../repositories/user-repository.js";
function getDefaultGuacUrl(): string {
return `${process.env.GUACD_HOST || "localhost"}:${process.env.GUACD_PORT || "4822"}`;
}
export type HostDefaults = {
useSocks5?: boolean;
socks5Host?: string;
@@ -66,119 +56,6 @@ export function registerUserSettingsRoutes(
router: Router,
authenticateJWT: RequestHandler,
): void {
/**
* @openapi
* /users/guacamole-settings:
* get:
* summary: Get Guacamole settings
* description: Returns current guacd enabled status and host:port URL. No authentication required.
* tags:
* - Users
* responses:
* 200:
* description: Guacamole settings.
* content:
* application/json:
* schema:
* type: object
* properties:
* enabled:
* type: boolean
* url:
* type: string
* 500:
* description: Failed to get guacamole settings.
*/
router.get("/guacamole-settings", authenticateJWT, async (_req, res) => {
try {
const settings = createCurrentSettingsRepository();
const enabled = await settings.getBoolean("guac_enabled", true);
const url = await settings.get("guac_url");
res.json({
enabled,
url: url ?? getDefaultGuacUrl(),
});
} catch (err) {
authLogger.error("Failed to get guacamole settings", err);
res.status(500).json({ error: "Failed to get guacamole settings" });
}
});
/**
* @openapi
* /users/guacamole-settings:
* patch:
* summary: Update Guacamole settings
* description: Admin-only. Updates guacd enabled status and/or host:port URL.
* tags:
* - Users
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* enabled:
* type: boolean
* url:
* type: string
* responses:
* 200:
* description: Guacamole settings updated.
* 403:
* description: Not authorized.
* 500:
* description: Failed to update guacamole settings.
*/
router.patch("/guacamole-settings", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
try {
const actor = await getAdminActor(userId);
if (!actor) {
return res.status(403).json({ error: "Not authorized" });
}
const { enabled, url } = req.body;
const settings = createCurrentSettingsRepository();
if (typeof enabled === "boolean") {
await settings.set("guac_enabled", enabled ? "true" : "false");
}
if (typeof url === "string") {
await settings.set("guac_url", url);
try {
await restartGuacServer();
} catch (err) {
authLogger.error(
"Failed to restart guac server after URL update",
err,
);
}
}
const currentEnabled = await settings.getBoolean("guac_enabled", true);
const currentUrl = await settings.get("guac_url");
const { ipAddress, userAgent } = getRequestMeta(req);
await logAudit({
userId,
username: actor.username ?? userId,
action: "update_guacamole_settings",
resourceType: "setting",
details: JSON.stringify({ enabled, url }),
ipAddress,
userAgent,
success: true,
});
res.json({
enabled: currentEnabled,
url: currentUrl ?? getDefaultGuacUrl(),
});
} catch (err) {
authLogger.error("Failed to update guacamole settings", err);
res.status(500).json({ error: "Failed to update guacamole settings" });
}
});
/**
* @openapi
* /users/log-level: