mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: finish the remote-desktop plugin (B14)
This commit is contained in:
1 parent
0af2c94983
commit
14eb558b85
215 files changed
+30029
-3921
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* What core needs from the Remote Desktop plugin, resolved through the plugin
|
||||
* registry rather than an import: restarting its guacd WebSocket server after
|
||||
* an admin edits the guacd URL setting.
|
||||
*
|
||||
* The plugin publishes "remote-desktop.sessions" through ctx.registry on
|
||||
* activate and the registry revokes it on deactivate. A restart while it is
|
||||
* off does nothing, because resurrecting a server the plugin does not own
|
||||
* would be worse than doing nothing.
|
||||
*/
|
||||
|
||||
import { consume } from "../plugins/registry.js";
|
||||
|
||||
export const GUACAMOLE_SESSIONS_KEY = "remote-desktop.sessions";
|
||||
|
||||
export interface GuacamoleSessionProvider {
|
||||
restart: () => Promise<void>;
|
||||
}
|
||||
|
||||
/** Restarts the plugin's guacd WebSocket server. A no-op while it is off. */
|
||||
export async function restartGuacamoleService(): Promise<void> {
|
||||
await consume<GuacamoleSessionProvider>(GUACAMOLE_SESSIONS_KEY)?.restart();
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* ctx.credentials: plaintext protocol credentials for a host, for a plugin
|
||||
* that hands them to a program core does not run (remote desktop gives them
|
||||
* to guacd). Needs credentials:read, the one critical capability, and every
|
||||
* call is audited whether it succeeds or not.
|
||||
*
|
||||
* The protocol credentials themselves stay in core, next to the host, because
|
||||
* core's sharing model (per-recipient secret snapshots, personal overrides)
|
||||
* is what decides which ones a shared recipient may use.
|
||||
*/
|
||||
|
||||
import type {
|
||||
PluginCredentials,
|
||||
PluginHostProtocol,
|
||||
PluginProtocolTarget,
|
||||
} from "@termix/plugin-sdk/backend";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
import { assertCapability } from "./permissions.js";
|
||||
import { getActor } from "./actor.js";
|
||||
|
||||
type AuditFn = (
|
||||
action: string,
|
||||
details: string,
|
||||
outcome: { success: boolean; errorMessage?: string },
|
||||
) => Promise<void>;
|
||||
|
||||
interface Deps {
|
||||
manifest: PluginManifest;
|
||||
audit: AuditFn;
|
||||
}
|
||||
|
||||
const PROTOCOLS: PluginHostProtocol[] = ["rdp", "vnc", "telnet"];
|
||||
|
||||
type HostRow = Record<string, unknown>;
|
||||
|
||||
function str(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function parseJumpHosts(value: unknown): Array<{ hostId: number }> {
|
||||
let parsed: unknown = value;
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed
|
||||
.map((hop) => Number((hop as { hostId?: unknown })?.hostId))
|
||||
.filter((id) => Number.isInteger(id) && id > 0)
|
||||
.map((hostId) => ({ hostId }));
|
||||
}
|
||||
|
||||
async function resolveOwnerAuth(
|
||||
host: HostRow,
|
||||
protocol: PluginHostProtocol,
|
||||
): Promise<PluginProtocolTarget["auth"]> {
|
||||
const credentialId = host[`${protocol}CredentialId`] as number | null;
|
||||
const authType =
|
||||
str(host[`${protocol}AuthType`]) ||
|
||||
(credentialId ? "credential" : "direct");
|
||||
let username = str(host[`${protocol}User`]);
|
||||
let password = str(host[`${protocol}Password`]);
|
||||
|
||||
if (authType === "credential" && credentialId) {
|
||||
const { createCurrentHostResolutionRepository } =
|
||||
await import("../database/repositories/factory.js");
|
||||
const credential =
|
||||
await createCurrentHostResolutionRepository().findCredentialByIdForUser(
|
||||
credentialId,
|
||||
host.userId as string,
|
||||
);
|
||||
// The domain never comes from a stored credential.
|
||||
if (credential?.username) username = credential.username;
|
||||
if (credential?.password) password = credential.password;
|
||||
}
|
||||
|
||||
if (protocol === "rdp") username ||= str(host.username);
|
||||
password ||= str(host.password);
|
||||
|
||||
return {
|
||||
authType,
|
||||
username,
|
||||
password,
|
||||
domain: str(host.rdpDomain) || str(host.domain),
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveRecipientAuth(
|
||||
host: HostRow,
|
||||
hostId: number,
|
||||
userId: string,
|
||||
protocol: PluginHostProtocol,
|
||||
): Promise<PluginProtocolTarget["auth"]> {
|
||||
const { resolveRecipientSharedHostAuthentication } =
|
||||
await import("../utils/shared-host-auth-resolver.js");
|
||||
// Recipients never read the owner's raw secrets, so start from nothing.
|
||||
const recipientHost = {
|
||||
...host,
|
||||
password: null,
|
||||
rdpUser: null,
|
||||
rdpPassword: null,
|
||||
vncUser: null,
|
||||
vncPassword: null,
|
||||
telnetUser: null,
|
||||
telnetPassword: null,
|
||||
};
|
||||
let resolution: Awaited<
|
||||
ReturnType<typeof resolveRecipientSharedHostAuthentication>
|
||||
>;
|
||||
try {
|
||||
resolution = await resolveRecipientSharedHostAuthentication(
|
||||
recipientHost as never,
|
||||
hostId,
|
||||
userId,
|
||||
protocol,
|
||||
);
|
||||
} catch {
|
||||
// Same as before the move: connect without stored credentials.
|
||||
resolution = { source: "required" };
|
||||
}
|
||||
|
||||
const auth =
|
||||
resolution.source === "personal-override"
|
||||
? { ...resolution.credential, domain: null }
|
||||
: resolution.source === "owner-shared"
|
||||
? resolution.secret
|
||||
: null;
|
||||
|
||||
let authType = "direct";
|
||||
if (resolution.source === "personal-override") authType = "credential";
|
||||
else if (resolution.source === "owner-shared") authType = resolution.authType;
|
||||
else if (
|
||||
resolution.source === "secretless" &&
|
||||
str(host[`${protocol}AuthType`]) === "none"
|
||||
) {
|
||||
authType = "none";
|
||||
}
|
||||
|
||||
return {
|
||||
authType,
|
||||
username: str(auth?.username),
|
||||
password: str(auth?.password),
|
||||
domain: str(auth?.domain) || str(host.rdpDomain) || str(host.domain),
|
||||
};
|
||||
}
|
||||
|
||||
export function createPluginCredentials({
|
||||
manifest,
|
||||
audit,
|
||||
}: Deps): PluginCredentials {
|
||||
const pluginId = manifest.id;
|
||||
const declared = manifest.capabilities;
|
||||
|
||||
return {
|
||||
resolveHostProtocol: async (hostId, protocol) => {
|
||||
const details = `${protocol} credentials for host ${hostId}`;
|
||||
try {
|
||||
await assertCapability(pluginId, "credentials:read", declared);
|
||||
if (!PROTOCOLS.includes(protocol)) {
|
||||
throw new Error(`Unsupported protocol: ${String(protocol)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
await audit("credentials_read", details, {
|
||||
success: false,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
const userId = getActor();
|
||||
if (!userId) {
|
||||
throw new Error(
|
||||
"ctx.credentials needs an acting user: call it inside a request or ctx.asUser",
|
||||
);
|
||||
}
|
||||
|
||||
const { createCurrentHostResolutionRepository } =
|
||||
await import("../database/repositories/factory.js");
|
||||
const repository = createCurrentHostResolutionRepository();
|
||||
// Shared hosts carry fields encrypted under the owner's key.
|
||||
const ownerId = await repository.findHostOwnerId(hostId);
|
||||
const host = ownerId
|
||||
? ((await repository.findHostById(hostId, ownerId)) as HostRow | null)
|
||||
: null;
|
||||
if (!host) {
|
||||
await audit("credentials_read", details, {
|
||||
success: false,
|
||||
errorMessage: "Host not found",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const shared = host.userId !== userId;
|
||||
if (shared) {
|
||||
const { PermissionManager } =
|
||||
await import("../utils/permission-manager.js");
|
||||
const access = await PermissionManager.getInstance().canAccessHost(
|
||||
userId,
|
||||
hostId,
|
||||
"connect",
|
||||
);
|
||||
if (!access.hasAccess) {
|
||||
await audit("credentials_read", details, {
|
||||
success: false,
|
||||
errorMessage: "No connect access",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const auth = shared
|
||||
? await resolveRecipientAuth(host, hostId, userId, protocol)
|
||||
: await resolveOwnerAuth(host, protocol);
|
||||
|
||||
await audit("credentials_read", details, { success: true });
|
||||
return {
|
||||
host: {
|
||||
id: hostId,
|
||||
name: (host.name as string | null) ?? null,
|
||||
ip: str(host.ip),
|
||||
port: Number(host.port) || 0,
|
||||
ownerUserId: host.userId as string,
|
||||
jumpHosts: parseJumpHosts(host.jumpHosts),
|
||||
},
|
||||
shared,
|
||||
auth,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -78,7 +78,6 @@ function toRecord(host: Record<string, unknown>): PluginHostRecord {
|
||||
folder: (host.folder as string | null) ?? null,
|
||||
jumpHosts: host.jumpHosts,
|
||||
enableSsh: (host.enableSsh as boolean | null) ?? null,
|
||||
enableRdp: (host.enableRdp as boolean | null) ?? null,
|
||||
enableTerminal: (host.enableTerminal as boolean | null) ?? null,
|
||||
enableFileManager: (host.enableFileManager as boolean | null) ?? null,
|
||||
enableTunnel: (host.enableTunnel as boolean | null) ?? null,
|
||||
@@ -205,6 +204,14 @@ export function createPluginHosts({ manifest, audit }: Deps): PluginHosts {
|
||||
...host,
|
||||
userId,
|
||||
});
|
||||
// Fields another plugin owns (a remote desktop switch) go to that
|
||||
// plugin's host settings, the same way a bulk import row's do.
|
||||
const { applyPluginHostImportSettings } =
|
||||
await import("../database/routes/host-plugin-settings.js");
|
||||
await applyPluginHostImportSettings(
|
||||
created.id,
|
||||
host as Record<string, unknown>,
|
||||
);
|
||||
await audit("hosts_create", `host ${created.id}`, { success: true });
|
||||
return toRecord(created as unknown as Record<string, unknown>);
|
||||
},
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
type PluginContext,
|
||||
type PluginModule,
|
||||
type PluginOpenIsolatedWindowRequest,
|
||||
type PluginNativeRdpRequest,
|
||||
} from "@termix/plugin-sdk/backend";
|
||||
import type { PluginTableDefinition } from "@termix/plugin-sdk/db";
|
||||
import * as syncRegistry from "./sync-registry.js";
|
||||
@@ -52,6 +53,8 @@ import {
|
||||
} from "../database/db/dialect.js";
|
||||
import { createPluginAuth, createPluginSsh } from "./ctx-ssh-auth.js";
|
||||
import { createPluginHosts } from "./ctx-hosts.js";
|
||||
import { createPluginCredentials } from "./ctx-credentials.js";
|
||||
import { isElectronIpcAvailable } from "../utils/electron-ipc-bridge.js";
|
||||
|
||||
export type { PluginModule };
|
||||
|
||||
@@ -336,6 +339,28 @@ export function createPluginContext(
|
||||
},
|
||||
);
|
||||
|
||||
const desktopLaunchNativeRdp = guarded(
|
||||
manifest,
|
||||
"desktop:window",
|
||||
async (request: PluginNativeRdpRequest) => {
|
||||
const { isElectronIpcAvailable, requestFromElectronMain } =
|
||||
await import("../utils/electron-ipc-bridge.js");
|
||||
if (!isElectronIpcAvailable()) {
|
||||
throw new Error(
|
||||
`Plugin ${pluginId} tried to open the native RDP client outside the desktop app`,
|
||||
);
|
||||
}
|
||||
return requestFromElectronMain<{ success: boolean; error?: string }>(
|
||||
"launch-native-rdp",
|
||||
request,
|
||||
);
|
||||
},
|
||||
{
|
||||
action: "desktop_launch_native_rdp",
|
||||
details: () => "opened the native RDP client",
|
||||
},
|
||||
);
|
||||
|
||||
const capabilitiesRequire = async (capability: string) => {
|
||||
try {
|
||||
await assertCapability(pluginId, capability, declared);
|
||||
@@ -696,8 +721,12 @@ export function createPluginContext(
|
||||
|
||||
desktop: {
|
||||
openIsolatedWindow: (request) => desktopOpenIsolatedWindow(request),
|
||||
launchNativeRdp: (request) => desktopLaunchNativeRdp(request),
|
||||
available: () => isElectronIpcAvailable(),
|
||||
},
|
||||
|
||||
credentials: createPluginCredentials({ manifest, audit: auditCall }),
|
||||
|
||||
audit: {
|
||||
// Attribution comes from the runtime: the actor, never a plugin value.
|
||||
record: async (entry) => {
|
||||
|
||||
@@ -40,7 +40,6 @@ export type { PluginSettingsScope };
|
||||
*/
|
||||
export const CORE_SETTINGS_ALLOWLIST: readonly string[] = [
|
||||
"app_name",
|
||||
"guac_enabled",
|
||||
"global_metrics_interval",
|
||||
"global_status_check_interval",
|
||||
];
|
||||
|
||||
@@ -377,6 +377,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
|
||||
const { runSessionRecordingSettingsMigration } =
|
||||
await import("./utils/crypto-migration/session-recording-settings-migration.js");
|
||||
await runSessionRecordingSettingsMigration();
|
||||
|
||||
const { runRemoteDesktopSettingsMigration } =
|
||||
await import("./utils/crypto-migration/remote-desktop-settings-migration.js");
|
||||
await runRemoteDesktopSettingsMigration();
|
||||
} catch (error) {
|
||||
systemLogger.warn("Plugin runtime failed to initialize", {
|
||||
operation: "plugin_init",
|
||||
|
||||
@@ -105,15 +105,10 @@ describe("normalizeProtocolEnableFields", () => {
|
||||
expect(normalizeProtocolEnableFields({ name: "server" })).toEqual({});
|
||||
});
|
||||
|
||||
it("converts explicitly provided protocol booleans to database integers", () => {
|
||||
it("converts an explicitly provided SSH switch to a database integer", () => {
|
||||
expect(
|
||||
normalizeProtocolEnableFields({
|
||||
enableSsh: true,
|
||||
enableRdp: false,
|
||||
enableVnc: undefined,
|
||||
enableTelnet: true,
|
||||
}),
|
||||
).toEqual({ enableSsh: 1, enableRdp: 0, enableTelnet: 1 });
|
||||
normalizeProtocolEnableFields({ enableSsh: true, enableRdp: false }),
|
||||
).toEqual({ enableSsh: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,14 +169,13 @@ describe("normalizeImportedHost", () => {
|
||||
expect(host.connectionType).toBe("ssh");
|
||||
expect(host.port).toBe(22);
|
||||
expect(host.enableSsh).toBe(true);
|
||||
expect(host.enableRdp).toBe(false);
|
||||
});
|
||||
|
||||
it("infers rdp from enableRdp and uses default rdp port", () => {
|
||||
const host = normalizeImportedHost({ enableRdp: true, ip: "10.0.0.2" });
|
||||
expect(host.connectionType).toBe("rdp");
|
||||
expect(host.port).toBe(3389);
|
||||
expect(host.enableRdp).toBe(true);
|
||||
expect(host.enableSsh).toBe(false);
|
||||
});
|
||||
|
||||
it("honors an explicit port over protocol defaults", () => {
|
||||
@@ -321,21 +315,15 @@ describe("transformHostResponse", () => {
|
||||
expect(result.jumpHosts).toEqual([]);
|
||||
});
|
||||
|
||||
it("infers protocol flags for a migrated non-ssh host", () => {
|
||||
const result = transformHostResponse({
|
||||
connectionType: "rdp",
|
||||
enableSsh: true,
|
||||
});
|
||||
expect(result.enableSsh).toBe(false);
|
||||
expect(result.enableRdp).toBe(true);
|
||||
it("passes the stored SSH switch through", () => {
|
||||
// The remote desktop migration corrects connection_type-only hosts once.
|
||||
expect(transformHostResponse({ enableSsh: false }).enableSsh).toBe(false);
|
||||
});
|
||||
|
||||
it("applies default protocol ports", () => {
|
||||
it("applies the default SSH port", () => {
|
||||
const result = transformHostResponse({ port: 22 });
|
||||
expect(result.sshPort).toBe(22);
|
||||
expect(result.rdpPort).toBe(3389);
|
||||
expect(result.vncPort).toBe(5900);
|
||||
expect(result.telnetPort).toBe(23);
|
||||
expect(result).not.toHaveProperty("rdpPort");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -426,8 +414,6 @@ describe("sanitizeHostForRecipient", () => {
|
||||
);
|
||||
expect(result.name).toBe("prod");
|
||||
expect(result.ip).toBe("10.0.0.42");
|
||||
expect(result.enableRdp).toBe(true);
|
||||
expect(result.rdpPort).toBe(3389);
|
||||
expect(result.rdpAuthType).toBe("none");
|
||||
expect(result.permissionLevel).toBe("connect");
|
||||
expect(result.shareSshAuth).toBe(true);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { buildRdpFile, validateNativeRdpOptions } =
|
||||
require("../../../../electron/native-rdp.cjs") as {
|
||||
buildRdpFile: (options: Record<string, unknown>) => string;
|
||||
validateNativeRdpOptions: (options: Record<string, unknown>) => unknown;
|
||||
};
|
||||
|
||||
describe("native Windows RDP", () => {
|
||||
it("creates a credential-prompting RDP file without a password", () => {
|
||||
const result = buildRdpFile({
|
||||
host: "server.example.com",
|
||||
port: 3390,
|
||||
username: "alice",
|
||||
domain: "EXAMPLE",
|
||||
password: "must-not-be-written",
|
||||
});
|
||||
|
||||
expect(result).toContain("full address:s:server.example.com:3390");
|
||||
expect(result).toContain("username:s:EXAMPLE\\alice");
|
||||
expect(result).toContain("prompt for credentials:i:1");
|
||||
expect(result).not.toContain("must-not-be-written");
|
||||
});
|
||||
|
||||
it("rejects unsafe host and port values", () => {
|
||||
expect(() =>
|
||||
validateNativeRdpOptions({ host: "bad\r\nvalue" }),
|
||||
).not.toThrow();
|
||||
expect(() => validateNativeRdpOptions({ host: "server/path" })).toThrow();
|
||||
expect(() =>
|
||||
validateNativeRdpOptions({ host: "server", port: 70000 }),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* ctx.credentials.resolveHostProtocol: credentials:read is checked and every
|
||||
* call audited, the owner gets their stored or credential-mode login, and a
|
||||
* shared recipient only ever sees what core's sharing rules hand them.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const grants = new Map<string, string[]>();
|
||||
const auditEntries: Array<Record<string, unknown>> = [];
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
hosts: new Map<number, Record<string, unknown>>(),
|
||||
credentials: new Map<number, { username: string; password: string }>(),
|
||||
canConnect: true,
|
||||
resolution: null as Record<string, unknown> | null,
|
||||
resolverCalls: [] as Array<Record<string, unknown>>,
|
||||
}));
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentPluginPermissionGrantRepository: () => ({
|
||||
listByPlugin: async (pluginId: string) =>
|
||||
(grants.get(pluginId) ?? []).map((capability) => ({
|
||||
pluginId,
|
||||
capability,
|
||||
})),
|
||||
}),
|
||||
createCurrentHostResolutionRepository: () => ({
|
||||
findHostOwnerId: async (hostId: number) =>
|
||||
(state.hosts.get(hostId)?.userId as string) ?? null,
|
||||
findHostById: async (hostId: number) => state.hosts.get(hostId) ?? null,
|
||||
findCredentialByIdForUser: async (id: number) =>
|
||||
state.credentials.get(id) ?? null,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/audit-logger.js", () => ({
|
||||
logAudit: async (entry: Record<string, unknown>) => {
|
||||
auditEntries.push(entry);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/permission-manager.js", () => ({
|
||||
PermissionManager: {
|
||||
getInstance: () => ({
|
||||
canAccessHost: async () => ({ hasAccess: state.canConnect }),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/shared-host-auth-resolver.js", () => ({
|
||||
resolveRecipientSharedHostAuthentication: async (
|
||||
host: Record<string, unknown>,
|
||||
) => {
|
||||
state.resolverCalls.push(host);
|
||||
return state.resolution;
|
||||
},
|
||||
}));
|
||||
|
||||
import { createPluginContext, createPluginHandle } from "../../plugins/ctx.js";
|
||||
import { invalidatePluginPermissionCache } from "../../plugins/permissions.js";
|
||||
import { runAsActor } from "../../plugins/actor.js";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
|
||||
function contextFor(capabilities: string[]) {
|
||||
const manifest = {
|
||||
id: "demo",
|
||||
name: "demo",
|
||||
version: "1.0.0",
|
||||
description: "",
|
||||
author: { name: "test" },
|
||||
license: "MIT",
|
||||
category: "Productivity",
|
||||
engine: { termix: ">=2.9.0", api: "1" },
|
||||
capabilities,
|
||||
} as PluginManifest;
|
||||
const handle = createPluginHandle("demo", { activate: () => {} });
|
||||
return createPluginContext(manifest, handle);
|
||||
}
|
||||
|
||||
const baseHost = {
|
||||
id: 7,
|
||||
userId: "owner",
|
||||
name: "Win box",
|
||||
ip: "10.0.0.7",
|
||||
port: 22,
|
||||
username: "sshuser",
|
||||
password: "ssh-secret",
|
||||
jumpHosts: JSON.stringify([{ hostId: 3 }]),
|
||||
rdpUser: "admin",
|
||||
rdpPassword: "rdp-secret",
|
||||
rdpDomain: "CORP",
|
||||
rdpAuthType: null,
|
||||
rdpCredentialId: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
grants.clear();
|
||||
auditEntries.length = 0;
|
||||
invalidatePluginPermissionCache();
|
||||
state.hosts.clear();
|
||||
state.credentials.clear();
|
||||
state.canConnect = true;
|
||||
state.resolution = null;
|
||||
state.resolverCalls.length = 0;
|
||||
state.hosts.set(7, { ...baseHost });
|
||||
});
|
||||
|
||||
describe("ctx.credentials.resolveHostProtocol", () => {
|
||||
it("refuses without credentials:read and audits the refusal", async () => {
|
||||
grants.set("demo", []);
|
||||
const ctx = contextFor([]);
|
||||
|
||||
await expect(
|
||||
runAsActor("owner", "request", () =>
|
||||
ctx.credentials.resolveHostProtocol(7, "rdp"),
|
||||
),
|
||||
).rejects.toThrow(/credentials:read/);
|
||||
expect(auditEntries.at(-1)).toMatchObject({
|
||||
action: "plugin_credentials_read",
|
||||
success: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the owner's stored login and audits it", async () => {
|
||||
grants.set("demo", ["credentials:read"]);
|
||||
const ctx = contextFor(["credentials:read"]);
|
||||
|
||||
const target = await runAsActor("owner", "request", () =>
|
||||
ctx.credentials.resolveHostProtocol(7, "rdp"),
|
||||
);
|
||||
expect(target).toEqual({
|
||||
host: {
|
||||
id: 7,
|
||||
name: "Win box",
|
||||
ip: "10.0.0.7",
|
||||
port: 22,
|
||||
ownerUserId: "owner",
|
||||
jumpHosts: [{ hostId: 3 }],
|
||||
},
|
||||
shared: false,
|
||||
auth: {
|
||||
authType: "direct",
|
||||
username: "admin",
|
||||
password: "rdp-secret",
|
||||
domain: "CORP",
|
||||
},
|
||||
});
|
||||
expect(auditEntries.at(-1)).toMatchObject({
|
||||
action: "plugin_credentials_read",
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the stored credential in credential mode", async () => {
|
||||
grants.set("demo", ["credentials:read"]);
|
||||
state.hosts.set(7, {
|
||||
...baseHost,
|
||||
vncCredentialId: 11,
|
||||
vncAuthType: "credential",
|
||||
});
|
||||
state.credentials.set(11, { username: "viewer", password: "vnc-pass" });
|
||||
const ctx = contextFor(["credentials:read"]);
|
||||
|
||||
const target = await runAsActor("owner", "request", () =>
|
||||
ctx.credentials.resolveHostProtocol(7, "vnc"),
|
||||
);
|
||||
expect(target?.auth).toMatchObject({
|
||||
authType: "credential",
|
||||
username: "viewer",
|
||||
password: "vnc-pass",
|
||||
});
|
||||
});
|
||||
|
||||
it("never hands a shared recipient the owner's raw secrets", async () => {
|
||||
grants.set("demo", ["credentials:read"]);
|
||||
state.resolution = {
|
||||
source: "owner-shared",
|
||||
authType: "direct",
|
||||
secret: { username: "shared", password: "snap", domain: null },
|
||||
};
|
||||
const ctx = contextFor(["credentials:read"]);
|
||||
|
||||
const target = await runAsActor("guest", "request", () =>
|
||||
ctx.credentials.resolveHostProtocol(7, "rdp"),
|
||||
);
|
||||
expect(target?.shared).toBe(true);
|
||||
expect(target?.auth).toMatchObject({
|
||||
username: "shared",
|
||||
password: "snap",
|
||||
});
|
||||
expect(state.resolverCalls[0]).toMatchObject({
|
||||
password: null,
|
||||
rdpUser: null,
|
||||
rdpPassword: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("gives a recipient with nothing shared an empty login", async () => {
|
||||
grants.set("demo", ["credentials:read"]);
|
||||
state.resolution = { source: "required" };
|
||||
const ctx = contextFor(["credentials:read"]);
|
||||
|
||||
const target = await runAsActor("guest", "request", () =>
|
||||
ctx.credentials.resolveHostProtocol(7, "telnet"),
|
||||
);
|
||||
expect(target?.auth.username).toBe("");
|
||||
expect(target?.auth.password).toBe("");
|
||||
});
|
||||
|
||||
it("returns null without connect access or for a missing host", async () => {
|
||||
grants.set("demo", ["credentials:read"]);
|
||||
state.canConnect = false;
|
||||
const ctx = contextFor(["credentials:read"]);
|
||||
|
||||
await expect(
|
||||
runAsActor("guest", "request", () =>
|
||||
ctx.credentials.resolveHostProtocol(7, "rdp"),
|
||||
),
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
runAsActor("owner", "request", () =>
|
||||
ctx.credentials.resolveHostProtocol(99, "rdp"),
|
||||
),
|
||||
).resolves.toBeNull();
|
||||
expect(auditEntries.at(-1)).toMatchObject({ success: false });
|
||||
});
|
||||
});
|
||||
@@ -121,3 +121,50 @@ describe("ctx.desktop.openIsolatedWindow", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ctx.desktop.launchNativeRdp", () => {
|
||||
const request = { host: "win.example.test", port: 3389, username: "bob" };
|
||||
|
||||
it("refuses without desktop:window", async () => {
|
||||
grants.set("demo", []);
|
||||
const { ctx } = contextFor("demo", []);
|
||||
|
||||
await expect(ctx.desktop.launchNativeRdp(request)).rejects.toThrow(
|
||||
/desktop:window/,
|
||||
);
|
||||
expect(bridge.requestFromElectronMain).not.toHaveBeenCalled();
|
||||
expect(auditEntries.at(-1)).toMatchObject({
|
||||
action: "plugin_desktop_launch_native_rdp",
|
||||
success: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses outside the desktop app", async () => {
|
||||
grants.set("demo", ["desktop:window"]);
|
||||
const { ctx } = contextFor("demo", ["desktop:window"]);
|
||||
|
||||
expect(ctx.desktop.available()).toBe(false);
|
||||
await expect(ctx.desktop.launchNativeRdp(request)).rejects.toThrow(
|
||||
/desktop app/,
|
||||
);
|
||||
});
|
||||
|
||||
it("relays to Electron main and audits success", async () => {
|
||||
grants.set("demo", ["desktop:window"]);
|
||||
bridge.available = true;
|
||||
const { ctx } = contextFor("demo", ["desktop:window"]);
|
||||
|
||||
expect(ctx.desktop.available()).toBe(true);
|
||||
await expect(ctx.desktop.launchNativeRdp(request)).resolves.toEqual({
|
||||
success: true,
|
||||
});
|
||||
expect(bridge.requestFromElectronMain).toHaveBeenCalledWith(
|
||||
"launch-native-rdp",
|
||||
request,
|
||||
);
|
||||
expect(auditEntries.at(-1)).toMatchObject({
|
||||
action: "plugin_desktop_launch_native_rdp",
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ const h = vi.hoisted(() => ({
|
||||
activities: [] as Array<Record<string, unknown>>,
|
||||
sessions: [] as number[],
|
||||
released: [] as number[],
|
||||
importRows: [] as Array<{ hostId: number; row: Record<string, unknown> }>,
|
||||
roles: [] as Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -110,6 +111,14 @@ vi.mock("../../hosts/host-session-status.js", () => ({
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("../../database/routes/host-plugin-settings.js", () => ({
|
||||
applyPluginHostImportSettings: async (
|
||||
hostId: number,
|
||||
row: Record<string, unknown>,
|
||||
) => {
|
||||
h.importRows.push({ hostId, row });
|
||||
},
|
||||
}));
|
||||
vi.mock("../../utils/shared-host-secrets-manager.js", () => ({
|
||||
SharedHostSecretsManager: {
|
||||
getInstance: () => ({
|
||||
@@ -325,6 +334,7 @@ describe("ctx.hosts", () => {
|
||||
port: 22,
|
||||
username: "root",
|
||||
authType: "password",
|
||||
enableRdp: true,
|
||||
});
|
||||
expect(created).toMatchObject({
|
||||
id: 99,
|
||||
@@ -332,6 +342,11 @@ describe("ctx.hosts", () => {
|
||||
userId: "user-1",
|
||||
});
|
||||
expect(h.created).toHaveLength(1);
|
||||
// Another plugin's fields go through its import normalizer.
|
||||
expect(h.importRows.at(-1)).toMatchObject({
|
||||
hostId: 99,
|
||||
row: { enableRdp: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("updates a host once granted hosts:write", async () => {
|
||||
|
||||
@@ -83,6 +83,24 @@ describe("contributes.settings", () => {
|
||||
expect(errors.join(" ")).toContain('Unknown field "wat"');
|
||||
});
|
||||
|
||||
it("accepts a hidden field and rejects a non-boolean hidden", () => {
|
||||
expect(
|
||||
validateManifest(
|
||||
withSettings({
|
||||
host: {
|
||||
fields: [{ key: "a", type: "number", labelKey: "k", hidden: true }],
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual([]);
|
||||
const errors = validateManifest(
|
||||
withSettings({
|
||||
admin: [{ key: "a", type: "string", labelKey: "k", hidden: "yes" }],
|
||||
}),
|
||||
);
|
||||
expect(errors.join(" ")).toContain("hidden must be a boolean");
|
||||
});
|
||||
|
||||
it("rejects an unknown scope", () => {
|
||||
const errors = validateManifest(
|
||||
withSettings({ global: [{ key: "a", type: "string", labelKey: "k" }] }),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
const db = vi.hoisted(() => ({ current: {} as Record<string, unknown> }));
|
||||
|
||||
vi.mock("../../database/db/index.js", () => ({
|
||||
getDb: () => db.current,
|
||||
}));
|
||||
|
||||
const { selectRows, runStatement } =
|
||||
await import("../../utils/crypto-migration/raw-rows.js");
|
||||
|
||||
describe("raw-rows", () => {
|
||||
beforeEach(() => {
|
||||
db.current = {};
|
||||
});
|
||||
|
||||
it("uses all() and run() on SQLite", async () => {
|
||||
const run = vi.fn();
|
||||
db.current = { all: async () => [{ id: 1 }], run };
|
||||
await expect(selectRows(sql`SELECT 1`)).resolves.toEqual([{ id: 1 }]);
|
||||
await runStatement(sql`UPDATE x SET y = 1`);
|
||||
expect(run).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("reads rows from node-postgres's { rows }", async () => {
|
||||
db.current = { execute: async () => ({ rows: [{ id: 2 }] }) };
|
||||
await expect(selectRows(sql`SELECT 1`)).resolves.toEqual([{ id: 2 }]);
|
||||
});
|
||||
|
||||
it("reads rows from mysql2's [rows, fields]", async () => {
|
||||
const execute = vi.fn(async () => [[{ id: 3 }], []]);
|
||||
db.current = { execute };
|
||||
await expect(selectRows(sql`SELECT 1`)).resolves.toEqual([{ id: 3 }]);
|
||||
await runStatement(sql`UPDATE x SET y = 1`);
|
||||
expect(execute).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* The Remote Desktop settings migration.
|
||||
*
|
||||
* An upgrade must be lossless: the admin switch and guacd address, each
|
||||
* user's RDP defaults and every host's RDP/VNC/Telnet options must come
|
||||
* through into the plugin's own settings, whatever shape an old host had.
|
||||
* Running it twice must not duplicate or clobber what it moved.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
interface Row {
|
||||
pluginId: string;
|
||||
scope: string;
|
||||
scopeId: string | null;
|
||||
key: string;
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
coreSettings: new Map<string, string>(),
|
||||
pluginRows: [] as Array<{
|
||||
pluginId: string;
|
||||
scope: string;
|
||||
scopeId: string | null;
|
||||
key: string;
|
||||
value: string | null;
|
||||
}>,
|
||||
hostRows: [] as Array<Record<string, unknown>>,
|
||||
userRows: [] as Array<{ user_id: string; rdp_defaults: string | null }>,
|
||||
statements: [] as string[],
|
||||
pluginInstalled: true,
|
||||
}));
|
||||
|
||||
const find = (scope: string, scopeId: string | null, key: string) =>
|
||||
state.pluginRows.find(
|
||||
(row) => row.scope === scope && row.scopeId === scopeId && row.key === key,
|
||||
);
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentPluginRepository: () => ({
|
||||
findById: async (id: string) =>
|
||||
state.pluginInstalled && id === "remote-desktop" ? { id } : null,
|
||||
}),
|
||||
createCurrentPluginSettingsRepository: () => ({
|
||||
get: async (
|
||||
_pluginId: string,
|
||||
scope: string,
|
||||
scopeId: string | null,
|
||||
key: string,
|
||||
) => find(scope, scopeId, key) ?? null,
|
||||
getAll: async (_pluginId: string, scope: string, scopeId: string | null) =>
|
||||
state.pluginRows.filter(
|
||||
(row) => row.scope === scope && row.scopeId === scopeId,
|
||||
),
|
||||
set: async (
|
||||
pluginId: string,
|
||||
scope: string,
|
||||
scopeId: string | null,
|
||||
key: string,
|
||||
value: string | null,
|
||||
) => {
|
||||
const existing = find(scope, scopeId, key);
|
||||
if (existing) {
|
||||
existing.value = value;
|
||||
return;
|
||||
}
|
||||
state.pluginRows.push({ pluginId, scope, scopeId, key, value });
|
||||
},
|
||||
}),
|
||||
createCurrentSettingsRepository: () => ({
|
||||
get: async (key: string) => state.coreSettings.get(key) ?? null,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/crypto-migration/raw-rows.js", () => ({
|
||||
selectRows: async (query: { queryChunks?: unknown[] }) => {
|
||||
const text = JSON.stringify(query.queryChunks ?? query);
|
||||
return text.includes("user_preferences") ? state.userRows : state.hostRows;
|
||||
},
|
||||
runStatement: async (query: unknown) => {
|
||||
state.statements.push(JSON.stringify(query));
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/logger.js", () => ({
|
||||
databaseLogger: { info: vi.fn(), warn: vi.fn() },
|
||||
}));
|
||||
|
||||
const { runRemoteDesktopSettingsMigration, hostSettingsFromRow } =
|
||||
await import("../../utils/crypto-migration/remote-desktop-settings-migration.js");
|
||||
|
||||
const value = (scope: string, scopeId: string | null, key: string) => {
|
||||
const row = find(scope, scopeId, key) as Row | undefined;
|
||||
return row?.value == null ? undefined : JSON.parse(row.value);
|
||||
};
|
||||
|
||||
function host(overrides: Record<string, unknown>) {
|
||||
return {
|
||||
id: 1,
|
||||
port: 22,
|
||||
connection_type: "ssh",
|
||||
enable_rdp: 0,
|
||||
enable_vnc: 0,
|
||||
enable_telnet: 0,
|
||||
rdp_port: 3389,
|
||||
vnc_port: 5900,
|
||||
telnet_port: 23,
|
||||
rdp_security: null,
|
||||
rdp_ignore_cert: 0,
|
||||
security: null,
|
||||
ignore_cert: 0,
|
||||
guacamole_config: null,
|
||||
enable_terminal_toolbar: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state.coreSettings.clear();
|
||||
state.pluginRows.length = 0;
|
||||
state.hostRows.length = 0;
|
||||
state.userRows.length = 0;
|
||||
state.statements.length = 0;
|
||||
state.pluginInstalled = true;
|
||||
});
|
||||
|
||||
describe("runRemoteDesktopSettingsMigration", () => {
|
||||
it("moves the admin switch and guacd address", async () => {
|
||||
state.coreSettings.set("guac_enabled", "false");
|
||||
state.coreSettings.set("guac_url", "guacd:4822");
|
||||
|
||||
const result = await runRemoteDesktopSettingsMigration();
|
||||
|
||||
expect(result.moved).toEqual(["guac_enabled", "guac_url"]);
|
||||
expect(value("admin", null, "enabled")).toBe(false);
|
||||
expect(value("admin", null, "guacdUrl")).toBe("guacd:4822");
|
||||
});
|
||||
|
||||
it("moves a host's options, reading SQLite's 0 and 1 as booleans", async () => {
|
||||
state.hostRows.push(
|
||||
host({
|
||||
id: 7,
|
||||
enable_rdp: 1,
|
||||
enable_vnc: 0,
|
||||
rdp_port: 3390,
|
||||
rdp_security: "nla",
|
||||
rdp_ignore_cert: 1,
|
||||
guacamole_config: JSON.stringify({ "color-depth": 24 }),
|
||||
enable_terminal_toolbar: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runRemoteDesktopSettingsMigration();
|
||||
|
||||
expect(result.hostsMoved).toBe(1);
|
||||
expect(value("host", "7", "enableRdp")).toBe(true);
|
||||
expect(value("host", "7", "enableVnc")).toBeUndefined();
|
||||
expect(value("host", "7", "rdpPort")).toBe(3390);
|
||||
expect(value("host", "7", "rdpSecurity")).toBe("nla");
|
||||
expect(value("host", "7", "rdpIgnoreCert")).toBe(true);
|
||||
expect(value("host", "7", "guacamoleConfig")).toEqual({
|
||||
"color-depth": 24,
|
||||
});
|
||||
expect(value("host", "7", "enableToolbar")).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves a plain SSH host alone", async () => {
|
||||
state.hostRows.push(host({ id: 3 }));
|
||||
const result = await runRemoteDesktopSettingsMigration();
|
||||
expect(result.hostsMoved).toBe(0);
|
||||
expect(state.pluginRows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("reads real booleans too, as Postgres returns them", async () => {
|
||||
state.hostRows.push(host({ id: 8, enable_vnc: true, vnc_port: 5901 }));
|
||||
await runRemoteDesktopSettingsMigration();
|
||||
expect(value("host", "8", "enableVnc")).toBe(true);
|
||||
expect(value("host", "8", "vncPort")).toBe(5901);
|
||||
});
|
||||
|
||||
it("moves each user's RDP defaults", async () => {
|
||||
state.userRows.push({
|
||||
user_id: "u1",
|
||||
rdp_defaults: JSON.stringify({
|
||||
colorDepth: 24,
|
||||
resizeMethod: "reconnect",
|
||||
disableCopy: true,
|
||||
enableWallpaper: false,
|
||||
}),
|
||||
});
|
||||
state.userRows.push({ user_id: "u2", rdp_defaults: null });
|
||||
|
||||
const result = await runRemoteDesktopSettingsMigration();
|
||||
|
||||
expect(result.usersMoved).toBe(1);
|
||||
expect(value("user", "u1", "colorDepth")).toBe("24");
|
||||
expect(value("user", "u1", "resizeMethod")).toBe("reconnect");
|
||||
expect(value("user", "u1", "disableCopy")).toBe("on");
|
||||
expect(value("user", "u1", "enableWallpaper")).toBe("off");
|
||||
});
|
||||
|
||||
it("changes nothing when run twice", async () => {
|
||||
state.coreSettings.set("guac_url", "guacd:4822");
|
||||
state.hostRows.push(host({ id: 7, enable_rdp: 1, rdp_port: 3390 }));
|
||||
state.userRows.push({
|
||||
user_id: "u1",
|
||||
rdp_defaults: JSON.stringify({ colorDepth: 16 }),
|
||||
});
|
||||
|
||||
await runRemoteDesktopSettingsMigration();
|
||||
const firstRun = JSON.stringify(state.pluginRows);
|
||||
// Changed after the first run: must not be overwritten by the old values.
|
||||
find("host", "7", "rdpPort")!.value = JSON.stringify(4000);
|
||||
const second = await runRemoteDesktopSettingsMigration();
|
||||
|
||||
expect(second.moved).toEqual([]);
|
||||
expect(second.usersMoved).toBe(0);
|
||||
expect(second.hostsMoved).toBe(0);
|
||||
expect(second.hostsSkipped).toBe(1);
|
||||
expect(value("host", "7", "rdpPort")).toBe(4000);
|
||||
expect(JSON.parse(firstRun)).toHaveLength(state.pluginRows.length);
|
||||
});
|
||||
|
||||
it("does nothing until the plugin row exists", async () => {
|
||||
state.pluginInstalled = false;
|
||||
state.coreSettings.set("guac_url", "guacd:4822");
|
||||
state.hostRows.push(host({ id: 7, enable_rdp: 1 }));
|
||||
await runRemoteDesktopSettingsMigration();
|
||||
expect(state.pluginRows).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hostSettingsFromRow", () => {
|
||||
it("turns a connection_type-only host into its protocol switch and port", () => {
|
||||
expect(
|
||||
hostSettingsFromRow(
|
||||
host({ connection_type: "vnc", port: 5905, vnc_port: null }),
|
||||
),
|
||||
).toEqual({ enableVnc: true, vncPort: 5905 });
|
||||
});
|
||||
|
||||
it("folds the older security and ignore_cert columns in", () => {
|
||||
expect(
|
||||
hostSettingsFromRow(
|
||||
host({
|
||||
enable_rdp: 1,
|
||||
rdp_security: null,
|
||||
security: "tls",
|
||||
rdp_ignore_cert: null,
|
||||
ignore_cert: 1,
|
||||
}),
|
||||
),
|
||||
).toEqual({ enableRdp: true, rdpSecurity: "tls", rdpIgnoreCert: true });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Raw SQL for boot migrations that read columns schema.ts no longer declares.
|
||||
*
|
||||
* getDb() is typed as the SQLite database, but on Postgres and MySQL it is
|
||||
* that engine's drizzle instance, which has execute() instead of all() and
|
||||
* run(). These two pick whichever exists.
|
||||
*/
|
||||
|
||||
import type { SQL } from "drizzle-orm";
|
||||
import { getDb } from "../../database/db/index.js";
|
||||
|
||||
type AnyDb = {
|
||||
all?: (query: SQL) => Promise<unknown[]> | unknown[];
|
||||
run?: (query: SQL) => unknown;
|
||||
execute?: (query: SQL) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export async function selectRows<T>(query: SQL): Promise<T[]> {
|
||||
const db = getDb() as unknown as AnyDb;
|
||||
if (typeof db.all === "function") return (await db.all(query)) as T[];
|
||||
const result = await db.execute!(query);
|
||||
// node-postgres returns { rows }, mysql2 returns [rows, fields].
|
||||
if (Array.isArray(result)) return result[0] as T[];
|
||||
return ((result as { rows?: unknown[] }).rows ?? []) as T[];
|
||||
}
|
||||
|
||||
export async function runStatement(query: SQL): Promise<void> {
|
||||
const db = getDb() as unknown as AnyDb;
|
||||
if (typeof db.run === "function") {
|
||||
await db.run(query);
|
||||
return;
|
||||
}
|
||||
await db.execute!(query);
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Moves Remote Desktop's settings out of core and into the remote-desktop
|
||||
* plugin's own settings.
|
||||
*
|
||||
* Admin keys from `settings`: guac_enabled and guac_url. Per user, the RDP
|
||||
* defaults JSON in user_preferences.rdp_defaults. Per host, the option
|
||||
* columns in ssh_data: the three protocol switches and ports, rdp_security,
|
||||
* rdp_ignore_cert and guacamole_config, folding in the older security,
|
||||
* ignore_cert and connection_type-only shapes, plus enable_terminal_toolbar,
|
||||
* which also drove the remote desktop toolbar.
|
||||
*
|
||||
* The logins (users, passwords, credential ids, auth types, domain) stay in
|
||||
* core, next to the host, where sharing needs them.
|
||||
*
|
||||
* The columns stay in the database, unused: core's drizzle migrations run
|
||||
* before this does, so dropping them there would lose the data first.
|
||||
*
|
||||
* Idempotent: an admin key already set, or a user or host that already has
|
||||
* any remote-desktop row, is skipped, so this is safe on every boot.
|
||||
*/
|
||||
|
||||
import { sql } from "drizzle-orm";
|
||||
import { databaseLogger } from "../logger.js";
|
||||
import {
|
||||
createCurrentPluginRepository,
|
||||
createCurrentPluginSettingsRepository,
|
||||
createCurrentSettingsRepository,
|
||||
} from "../../database/repositories/factory.js";
|
||||
import { runStatement, selectRows } from "./raw-rows.js";
|
||||
|
||||
const PLUGIN_ID = "remote-desktop";
|
||||
const PROTOCOLS = ["rdp", "vnc", "telnet"] as const;
|
||||
const DEFAULT_PORTS = { rdp: 3389, vnc: 5900, telnet: 23 };
|
||||
|
||||
type Flag = number | boolean | string | null | undefined;
|
||||
|
||||
/** true, 1 and "1" are on; null means the column was never set. */
|
||||
function flag(value: Flag): boolean | undefined {
|
||||
if (value === null || value === undefined) return undefined;
|
||||
return value === true || value === 1 || value === "1" || value === "true";
|
||||
}
|
||||
|
||||
function port(value: unknown): number | undefined {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535
|
||||
? parsed
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function jsonObject(value: unknown): Record<string, unknown> | undefined {
|
||||
if (typeof value !== "string" || !value) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
||||
? parsed
|
||||
: undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
interface LegacyHostRow {
|
||||
id: number;
|
||||
port: number | null;
|
||||
connection_type: string | null;
|
||||
enable_rdp: Flag;
|
||||
enable_vnc: Flag;
|
||||
enable_telnet: Flag;
|
||||
rdp_port: number | null;
|
||||
vnc_port: number | null;
|
||||
telnet_port: number | null;
|
||||
rdp_security: string | null;
|
||||
rdp_ignore_cert: Flag;
|
||||
security: string | null;
|
||||
ignore_cert: Flag;
|
||||
guacamole_config: string | null;
|
||||
enable_terminal_toolbar: Flag;
|
||||
}
|
||||
|
||||
/** The plugin's host settings for one legacy row, defaults left out. */
|
||||
export function hostSettingsFromRow(
|
||||
row: LegacyHostRow,
|
||||
): Record<string, unknown> {
|
||||
const values: Record<string, unknown> = {};
|
||||
const flags = {
|
||||
rdp: flag(row.enable_rdp) === true,
|
||||
vnc: flag(row.enable_vnc) === true,
|
||||
telnet: flag(row.enable_telnet) === true,
|
||||
};
|
||||
const legacyType = PROTOCOLS.find((p) => p === row.connection_type);
|
||||
// Hosts from before the per-protocol switches only had connection_type.
|
||||
if (!flags.rdp && !flags.vnc && !flags.telnet && legacyType) {
|
||||
flags[legacyType] = true;
|
||||
}
|
||||
const ports = {
|
||||
rdp: port(row.rdp_port),
|
||||
vnc: port(row.vnc_port),
|
||||
telnet: port(row.telnet_port),
|
||||
};
|
||||
if (legacyType && ports[legacyType] === undefined) {
|
||||
ports[legacyType] = port(row.port);
|
||||
}
|
||||
|
||||
for (const protocol of PROTOCOLS) {
|
||||
const key = `enable${protocol[0].toUpperCase()}${protocol.slice(1)}`;
|
||||
if (flags[protocol]) values[key] = true;
|
||||
const value = ports[protocol];
|
||||
if (value !== undefined && value !== DEFAULT_PORTS[protocol]) {
|
||||
values[`${protocol}Port`] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const security = row.rdp_security || row.security;
|
||||
if (security) values.rdpSecurity = security;
|
||||
const ignoreCert = flag(row.rdp_ignore_cert) ?? flag(row.ignore_cert);
|
||||
if (ignoreCert) values.rdpIgnoreCert = true;
|
||||
const config = jsonObject(row.guacamole_config);
|
||||
if (config && Object.keys(config).length > 0) values.guacamoleConfig = config;
|
||||
if (flag(row.enable_terminal_toolbar) === false) values.enableToolbar = false;
|
||||
return values;
|
||||
}
|
||||
|
||||
/** Old camelCase defaults JSON -> the plugin's user settings. */
|
||||
export function userSettingsFromDefaults(
|
||||
defaults: Record<string, unknown>,
|
||||
): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
const depth = Number(defaults.colorDepth);
|
||||
if ([16, 24, 32].includes(depth)) values.colorDepth = String(depth);
|
||||
if (
|
||||
defaults.resizeMethod === "display-update" ||
|
||||
defaults.resizeMethod === "reconnect"
|
||||
) {
|
||||
values.resizeMethod = defaults.resizeMethod;
|
||||
}
|
||||
for (const key of [
|
||||
"forceLossless",
|
||||
"enableWallpaper",
|
||||
"enableFontSmoothing",
|
||||
"enableDesktopComposition",
|
||||
"disableAudio",
|
||||
"enablePrinting",
|
||||
"enableDrive",
|
||||
"disableCopy",
|
||||
"disablePaste",
|
||||
]) {
|
||||
if (typeof defaults[key] === "boolean") {
|
||||
values[key] = defaults[key] ? "on" : "off";
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export interface RemoteDesktopSettingsMigrationResult {
|
||||
moved: string[];
|
||||
usersMoved: number;
|
||||
hostsMoved: number;
|
||||
hostsSkipped: number;
|
||||
}
|
||||
|
||||
export async function runRemoteDesktopSettingsMigration(): Promise<RemoteDesktopSettingsMigrationResult> {
|
||||
const result: RemoteDesktopSettingsMigrationResult = {
|
||||
moved: [],
|
||||
usersMoved: 0,
|
||||
hostsMoved: 0,
|
||||
hostsSkipped: 0,
|
||||
};
|
||||
|
||||
// plugin_settings has a foreign key to plugins: nothing to migrate into
|
||||
// until the plugin row exists.
|
||||
try {
|
||||
const plugin = await createCurrentPluginRepository().findById(PLUGIN_ID);
|
||||
if (!plugin) return result;
|
||||
} catch {
|
||||
return result;
|
||||
}
|
||||
|
||||
const pluginSettings = createCurrentPluginSettingsRepository();
|
||||
const write = (
|
||||
scope: "admin" | "user" | "host",
|
||||
scopeId: string | null,
|
||||
key: string,
|
||||
value: unknown,
|
||||
) =>
|
||||
pluginSettings.set(PLUGIN_ID, scope, scopeId, key, JSON.stringify(value));
|
||||
|
||||
try {
|
||||
const settings = createCurrentSettingsRepository();
|
||||
const moves: [string, string, (raw: string) => unknown][] = [
|
||||
["guac_enabled", "enabled", (raw) => raw !== "false" && raw !== "0"],
|
||||
["guac_url", "guacdUrl", (raw) => raw],
|
||||
];
|
||||
for (const [legacyKey, field, parse] of moves) {
|
||||
const legacy = await settings.get(legacyKey);
|
||||
if (legacy === null || legacy === "") continue;
|
||||
const existing = await pluginSettings.get(
|
||||
PLUGIN_ID,
|
||||
"admin",
|
||||
null,
|
||||
field,
|
||||
);
|
||||
if (existing && existing.value !== null) continue;
|
||||
await write("admin", null, field, parse(legacy));
|
||||
result.moved.push(legacyKey);
|
||||
}
|
||||
} catch (error) {
|
||||
databaseLogger.warn("Remote Desktop admin settings migration failed", {
|
||||
operation: "remote_desktop_settings_migration",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = await selectRows<{
|
||||
user_id: string;
|
||||
rdp_defaults: string | null;
|
||||
}>(sql`SELECT user_id, rdp_defaults FROM user_preferences`);
|
||||
for (const row of rows) {
|
||||
const defaults = jsonObject(row.rdp_defaults);
|
||||
if (!defaults) continue;
|
||||
const values = userSettingsFromDefaults(defaults);
|
||||
if (Object.keys(values).length === 0) continue;
|
||||
const existing = await pluginSettings.getAll(
|
||||
PLUGIN_ID,
|
||||
"user",
|
||||
row.user_id,
|
||||
);
|
||||
if (existing.length > 0) continue;
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
await write("user", row.user_id, key, value);
|
||||
}
|
||||
result.usersMoved++;
|
||||
}
|
||||
} catch {
|
||||
// A fresh install never had the column.
|
||||
}
|
||||
|
||||
try {
|
||||
// Raw SQL: schema.ts no longer declares these columns.
|
||||
const rows = await selectRows<LegacyHostRow>(sql`
|
||||
SELECT id, port, connection_type, enable_rdp, enable_vnc, enable_telnet,
|
||||
rdp_port, vnc_port, telnet_port, rdp_security, rdp_ignore_cert,
|
||||
security, ignore_cert, guacamole_config, enable_terminal_toolbar
|
||||
FROM ssh_data
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const values = hostSettingsFromRow(row);
|
||||
if (Object.keys(values).length === 0) continue;
|
||||
const scopeId = String(row.id);
|
||||
const existing = await pluginSettings.getAll(PLUGIN_ID, "host", scopeId);
|
||||
if (existing.length > 0) {
|
||||
result.hostsSkipped++;
|
||||
continue;
|
||||
}
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
await write("host", scopeId, key, value);
|
||||
}
|
||||
result.hostsMoved++;
|
||||
}
|
||||
|
||||
// Those same connection_type-only hosts kept the enable_ssh default of
|
||||
// on, which core used to correct on every read. FALSE and NOT read the
|
||||
// same on SQLite's integers and on real booleans.
|
||||
await runStatement(sql`
|
||||
UPDATE ssh_data SET enable_ssh = FALSE
|
||||
WHERE connection_type IN ('rdp', 'vnc', 'telnet')
|
||||
AND (enable_rdp IS NULL OR NOT enable_rdp)
|
||||
AND (enable_vnc IS NULL OR NOT enable_vnc)
|
||||
AND (enable_telnet IS NULL OR NOT enable_telnet)
|
||||
`);
|
||||
} catch {
|
||||
// A fresh install never had the columns.
|
||||
}
|
||||
|
||||
if (
|
||||
result.moved.length > 0 ||
|
||||
result.usersMoved > 0 ||
|
||||
result.hostsMoved > 0
|
||||
) {
|
||||
databaseLogger.info(
|
||||
`Moved ${result.moved.length} Remote Desktop setting(s), ${result.usersMoved} user(s) and ${result.hostsMoved} host(s) into plugin settings`,
|
||||
{ operation: "remote_desktop_settings_migration" },
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
export type GuacdOptions = {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
|
||||
const DEFAULT_GUACD_OPTIONS: GuacdOptions = {
|
||||
host: "localhost",
|
||||
port: 4822,
|
||||
};
|
||||
|
||||
function parsePort(value: string | undefined, fallback: number) {
|
||||
const port = parseInt(value || "", 10);
|
||||
return Number.isFinite(port) ? port : fallback;
|
||||
}
|
||||
|
||||
export function parseGuacdUrl(
|
||||
value: string,
|
||||
fallback: GuacdOptions = DEFAULT_GUACD_OPTIONS,
|
||||
): GuacdOptions {
|
||||
const raw = value.trim();
|
||||
if (!raw) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (raw.includes("://")) {
|
||||
try {
|
||||
const url = new URL(raw);
|
||||
return {
|
||||
host: url.hostname || fallback.host,
|
||||
port: parsePort(url.port, fallback.port),
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const bracketedIpv6 = raw.match(/^\[([^\]]+)\](?::(\d+))?$/);
|
||||
if (bracketedIpv6) {
|
||||
return {
|
||||
host: bracketedIpv6[1],
|
||||
port: parsePort(bracketedIpv6[2], fallback.port),
|
||||
};
|
||||
}
|
||||
|
||||
const [host, port] = raw.split(":");
|
||||
return {
|
||||
host: host || fallback.host,
|
||||
port: parsePort(port, fallback.port),
|
||||
};
|
||||
}
|
||||
|
||||
export function getGuacdEnvOptions(): GuacdOptions | null {
|
||||
if (process.env.GUACD_URL) {
|
||||
return parseGuacdUrl(process.env.GUACD_URL);
|
||||
}
|
||||
|
||||
if (!process.env.GUACD_HOST && !process.env.GUACD_PORT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
host: process.env.GUACD_HOST || DEFAULT_GUACD_OPTIONS.host,
|
||||
port: parsePort(process.env.GUACD_PORT, DEFAULT_GUACD_OPTIONS.port),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveGuacdOptions(dbUrl?: string | null): GuacdOptions {
|
||||
const envOptions = getGuacdEnvOptions();
|
||||
if (envOptions) {
|
||||
return envOptions;
|
||||
}
|
||||
|
||||
if (dbUrl) {
|
||||
return parseGuacdUrl(dbUrl);
|
||||
}
|
||||
|
||||
return DEFAULT_GUACD_OPTIONS;
|
||||
}
|
||||
|
||||
export function formatGuacdOptions(options: GuacdOptions): string {
|
||||
return `${options.host}:${options.port}`;
|
||||
}
|
||||
|
||||
export function getDefaultGuacdUrl(): string {
|
||||
return formatGuacdOptions(resolveGuacdOptions());
|
||||
}
|
||||
@@ -39,22 +39,19 @@ function snapshotRecordId(
|
||||
return `shared-${hostAccessId}-${targetUserId}-${protocol}`;
|
||||
}
|
||||
|
||||
// Mirrors the connection-type migration fallback in transformHostResponse():
|
||||
// old hosts only set connectionType, the per-protocol enable flags came later.
|
||||
// SSH follows its own switch. The remote desktop protocols' switches are the
|
||||
// remote-desktop plugin's host settings, and the host editor clears a
|
||||
// protocol's login when it is switched off, so a snapshot is taken whenever
|
||||
// login data is present (see collectProtocolSnapshots).
|
||||
function enabledProtocols(
|
||||
host: HostResolutionHostRecord,
|
||||
): Record<ShareProtocol, boolean> {
|
||||
const ct = host.connectionType;
|
||||
const rdp = !!host.enableRdp;
|
||||
const vnc = !!host.enableVnc;
|
||||
const telnet = !!host.enableTelnet;
|
||||
const isMigratedNonSsh = !rdp && !vnc && !telnet && !!ct && ct !== "ssh";
|
||||
|
||||
return {
|
||||
ssh: isMigratedNonSsh ? false : host.enableSsh !== false,
|
||||
rdp: isMigratedNonSsh ? ct === "rdp" : rdp,
|
||||
vnc: isMigratedNonSsh ? ct === "vnc" : vnc,
|
||||
telnet: isMigratedNonSsh ? ct === "telnet" : telnet,
|
||||
ssh: host.enableSsh !== false && (!ct || ct === "ssh"),
|
||||
rdp: true,
|
||||
vnc: true,
|
||||
telnet: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,29 +8,24 @@ export const SUPPORTED_AUTH_OVERRIDE_PROTOCOLS =
|
||||
export const AUTH_PROTOCOL_METADATA = {
|
||||
ssh: {
|
||||
label: "SSH",
|
||||
enableField: "enableSsh",
|
||||
credentialField: "credentialId",
|
||||
},
|
||||
rdp: {
|
||||
label: "RDP",
|
||||
enableField: "enableRdp",
|
||||
credentialField: "rdpCredentialId",
|
||||
},
|
||||
vnc: {
|
||||
label: "VNC",
|
||||
enableField: "enableVnc",
|
||||
credentialField: "vncCredentialId",
|
||||
},
|
||||
telnet: {
|
||||
label: "Telnet",
|
||||
enableField: "enableTelnet",
|
||||
credentialField: "telnetCredentialId",
|
||||
},
|
||||
} as const satisfies Record<
|
||||
AuthOverrideProtocol,
|
||||
{
|
||||
label: string;
|
||||
enableField: "enableSsh" | "enableRdp" | "enableVnc" | "enableTelnet";
|
||||
credentialField:
|
||||
| "credentialId"
|
||||
| "rdpCredentialId"
|
||||
|
||||
Vendored
-6
@@ -50,12 +50,6 @@ export type LocalPathMutationResult =
|
||||
export interface ElectronAPI {
|
||||
getAppVersion: () => Promise<string>;
|
||||
getPlatform: () => Promise<string>;
|
||||
openNativeRdp: (options: {
|
||||
host: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
domain?: string;
|
||||
}) => Promise<{ success: boolean; error?: string }>;
|
||||
getSetting?: (key: string) => Promise<string | null | undefined>;
|
||||
setSetting?: (key: string, value: string) => Promise<void>;
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Per-host Guacamole connection settings, as edited in the host editor and
|
||||
* forwarded to guacd. Field names are camelCase here and translated to the
|
||||
* hyphenated protocol parameters in toGuacamoleParams().
|
||||
*/
|
||||
export interface GuacamoleConfig {
|
||||
colorDepth?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
dpi?: number;
|
||||
resizeMethod?: string;
|
||||
forceLossless?: boolean;
|
||||
disableAudio?: boolean;
|
||||
enableAudioInput?: boolean;
|
||||
enableWallpaper?: boolean;
|
||||
enableTheming?: boolean;
|
||||
enableFontSmoothing?: boolean;
|
||||
enableFullWindowDrag?: boolean;
|
||||
enableDesktopComposition?: boolean;
|
||||
enableMenuAnimations?: boolean;
|
||||
disableBitmapCaching?: boolean;
|
||||
disableOffscreenCaching?: boolean;
|
||||
disableGlyphCaching?: boolean;
|
||||
disableGfx?: boolean;
|
||||
enablePrinting?: boolean;
|
||||
printerName?: string;
|
||||
enableDrive?: boolean;
|
||||
driveName?: string;
|
||||
drivePath?: string;
|
||||
createDrivePath?: boolean;
|
||||
disableDownload?: boolean;
|
||||
disableUpload?: boolean;
|
||||
enableTouch?: boolean;
|
||||
clientName?: string;
|
||||
console?: boolean;
|
||||
initialProgram?: string;
|
||||
serverLayout?: string;
|
||||
timezone?: string;
|
||||
gatewayHostname?: string;
|
||||
gatewayPort?: number;
|
||||
gatewayUsername?: string;
|
||||
gatewayPassword?: string;
|
||||
gatewayDomain?: string;
|
||||
remoteApp?: string;
|
||||
remoteAppDir?: string;
|
||||
remoteAppArgs?: string;
|
||||
normalizeClipboard?: string;
|
||||
disableCopy?: boolean;
|
||||
disablePaste?: boolean;
|
||||
cursor?: string;
|
||||
swapRedBlue?: boolean;
|
||||
readOnly?: boolean;
|
||||
recordingPath?: string;
|
||||
recordingName?: string;
|
||||
createRecordingPath?: boolean;
|
||||
recordingExcludeOutput?: boolean;
|
||||
recordingExcludeMouse?: boolean;
|
||||
recordingIncludeKeys?: boolean;
|
||||
wolSendPacket?: boolean;
|
||||
wolMacAddr?: string;
|
||||
wolBroadcastAddr?: string;
|
||||
wolUdpPort?: number;
|
||||
wolWaitTime?: number;
|
||||
}
|
||||
@@ -36,7 +36,8 @@ export interface HostSidebarFilterState {
|
||||
status: ("online" | "offline" | "pinned")[];
|
||||
/** SSH auth type ids; plugins add their own. */
|
||||
authType: string[];
|
||||
protocol: ("ssh" | "rdp" | "vnc" | "telnet")[];
|
||||
/** "ssh" or a plugin protocol id. */
|
||||
protocol: string[];
|
||||
features: ("terminal" | "fileManager" | "tunnel" | "docker")[];
|
||||
tags: string[];
|
||||
}
|
||||
@@ -93,14 +94,8 @@ const FILTER_STATUS: HostSidebarFilterState["status"] = [
|
||||
"offline",
|
||||
"pinned",
|
||||
];
|
||||
/** Same shape as a manifest's contributes.auth.sshAuthTypes entries. */
|
||||
/** Same shape as a manifest's auth type and protocol ids. */
|
||||
const AUTH_TYPE_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
||||
const FILTER_PROTOCOL: HostSidebarFilterState["protocol"] = [
|
||||
"ssh",
|
||||
"rdp",
|
||||
"vnc",
|
||||
"telnet",
|
||||
];
|
||||
const FILTER_FEATURES: HostSidebarFilterState["features"] = [
|
||||
"terminal",
|
||||
"fileManager",
|
||||
@@ -174,7 +169,9 @@ export function sanitizeHostSidebarPreferences(
|
||||
authType: sanitizeStringArray(filtersObj.authType).filter((value) =>
|
||||
AUTH_TYPE_PATTERN.test(value),
|
||||
),
|
||||
protocol: sanitizeEnumArray(filtersObj.protocol, FILTER_PROTOCOL),
|
||||
protocol: sanitizeStringArray(filtersObj.protocol).filter((value) =>
|
||||
AUTH_TYPE_PATTERN.test(value),
|
||||
),
|
||||
features: sanitizeEnumArray(filtersObj.features, FILTER_FEATURES),
|
||||
tags: sanitizeStringArray(filtersObj.tags),
|
||||
};
|
||||
|
||||
+4
-25
@@ -1,4 +1,3 @@
|
||||
import type { GuacamoleConfig } from "./guacamole-config.js";
|
||||
import type { StatsConfig } from "./stats-widgets.js";
|
||||
import type { Request } from "express";
|
||||
import type { RefObject } from "react";
|
||||
@@ -250,29 +249,19 @@ export type Host = {
|
||||
delay?: number;
|
||||
}>;
|
||||
|
||||
connectionType?: "ssh" | "rdp" | "vnc" | "telnet";
|
||||
/** "ssh", or the id of the plugin protocol a host without SSH uses. */
|
||||
connectionType?: string;
|
||||
domain?: string;
|
||||
security?: string;
|
||||
ignoreCert?: boolean;
|
||||
guacamoleConfig?: string | GuacamoleConfig;
|
||||
dockerConfig?: Record<string, unknown> | null;
|
||||
enableWebUi?: boolean;
|
||||
webUiConfig?: WebUiConfig | null;
|
||||
|
||||
enableSsh?: boolean;
|
||||
enableRdp?: boolean;
|
||||
enableVnc?: boolean;
|
||||
enableTelnet?: boolean;
|
||||
sshPort?: number;
|
||||
rdpPort?: number;
|
||||
vncPort?: number;
|
||||
telnetPort?: number;
|
||||
rdpCredentialId?: number | null;
|
||||
rdpUser?: string;
|
||||
rdpPassword?: string;
|
||||
rdpDomain?: string;
|
||||
rdpSecurity?: string;
|
||||
rdpIgnoreCert?: boolean;
|
||||
vncCredentialId?: number | null;
|
||||
vncPassword?: string;
|
||||
vncUser?: string;
|
||||
@@ -415,29 +404,19 @@ export interface HostData {
|
||||
delay?: number;
|
||||
}>;
|
||||
|
||||
connectionType?: "ssh" | "rdp" | "vnc" | "telnet";
|
||||
/** "ssh", or the id of the plugin protocol a host without SSH uses. */
|
||||
connectionType?: string;
|
||||
domain?: string;
|
||||
security?: string;
|
||||
ignoreCert?: boolean;
|
||||
guacamoleConfig?: GuacamoleConfig | null;
|
||||
dockerConfig?: Record<string, unknown> | null;
|
||||
enableWebUi?: boolean;
|
||||
webUiConfig?: WebUiConfig | null;
|
||||
|
||||
enableSsh?: boolean;
|
||||
enableRdp?: boolean;
|
||||
enableVnc?: boolean;
|
||||
enableTelnet?: boolean;
|
||||
sshPort?: number;
|
||||
rdpPort?: number;
|
||||
vncPort?: number;
|
||||
telnetPort?: number;
|
||||
rdpCredentialId?: number | null;
|
||||
rdpUser?: string;
|
||||
rdpPassword?: string;
|
||||
rdpDomain?: string;
|
||||
rdpSecurity?: string;
|
||||
rdpIgnoreCert?: boolean;
|
||||
vncCredentialId?: number | null;
|
||||
vncPassword?: string;
|
||||
vncUser?: string;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { GuacamoleConfig } from "./guacamole-config.js";
|
||||
import type { TerminalConfig } from "./index.js";
|
||||
import type { StatsConfig } from "./stats-widgets.js";
|
||||
import type { HostAuthOverrides } from "./auth-protocols.js";
|
||||
@@ -152,14 +151,8 @@ export type Host = {
|
||||
quickActions: { name: string; snippetId: string }[];
|
||||
|
||||
enableSsh: boolean;
|
||||
enableRdp: boolean;
|
||||
enableVnc: boolean;
|
||||
enableTelnet: boolean;
|
||||
|
||||
sshPort: number;
|
||||
rdpPort: number;
|
||||
vncPort: number;
|
||||
telnetPort: number;
|
||||
|
||||
rdpAuthType?: "direct" | "credential" | "none";
|
||||
rdpCredentialId?: string;
|
||||
@@ -167,8 +160,6 @@ export type Host = {
|
||||
rdpPassword?: string;
|
||||
hasRdpPassword?: boolean;
|
||||
domain?: string;
|
||||
security?: string;
|
||||
ignoreCert?: boolean;
|
||||
|
||||
vncAuthType?: "direct" | "credential";
|
||||
vncCredentialId?: string;
|
||||
@@ -182,7 +173,6 @@ export type Host = {
|
||||
telnetPassword?: string;
|
||||
hasTelnetPassword?: boolean;
|
||||
|
||||
guacamoleConfig?: GuacamoleConfig;
|
||||
/** Host-scope plugin settings, keyed by plugin id. Secrets are redacted. */
|
||||
pluginSettings?: Record<string, Record<string, unknown>>;
|
||||
forceKeyboardInteractive?: boolean;
|
||||
|
||||
@@ -2497,13 +2497,7 @@ export function AppShell({
|
||||
enableTerminalToolbar: false,
|
||||
enableAiAssistant: false,
|
||||
enableSsh: false,
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
sshPort: 22,
|
||||
rdpPort: 3389,
|
||||
vncPort: 5900,
|
||||
telnetPort: 23,
|
||||
serverTunnels: [],
|
||||
quickActions: [],
|
||||
};
|
||||
|
||||
@@ -112,7 +112,6 @@ export async function getAllServerStatuses(): Promise<
|
||||
try {
|
||||
const response = await sshHostApi.get<SSHHost[]>("/db/host");
|
||||
const defaultOrigin = await resolveConnectionOrigin({
|
||||
connectionType: "ssh",
|
||||
connectionOrigin: null,
|
||||
});
|
||||
localHostIds = (response.data || [])
|
||||
|
||||
@@ -146,7 +146,6 @@ export interface UserPreferences {
|
||||
customThemes?: string | null;
|
||||
customKeybindings?: string | null;
|
||||
terminalDefaults?: string | null;
|
||||
rdpDefaults?: string | null;
|
||||
terminalMacros?: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface PluginSettingsField {
|
||||
permission?: string;
|
||||
group?: string;
|
||||
component?: string;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginHostSettingsContribution {
|
||||
|
||||
@@ -106,33 +106,6 @@ export async function updateSessionTimeout(
|
||||
// Tailscale settings/device API wrappers moved to
|
||||
// plugins/tailscale/src/frontend/tailscale-api.ts.
|
||||
|
||||
// ============================================================================
|
||||
// GUACAMOLE SETTINGS
|
||||
// ============================================================================
|
||||
|
||||
export async function getGuacamoleSettings(): Promise<{
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await authApi.get("/users/guacamole-settings");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "fetch guacamole settings");
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateGuacamoleSettings(settings: {
|
||||
enabled?: boolean;
|
||||
url?: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await authApi.patch("/users/guacamole-settings", settings);
|
||||
} catch (error) {
|
||||
handleApiError(error, "update guacamole settings");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ANALYTICS SETTINGS
|
||||
// ============================================================================
|
||||
|
||||
@@ -10,27 +10,21 @@ import {
|
||||
} from "react";
|
||||
import { getUserPreferences, saveUserPreferences } from "@/api/open-tabs-api";
|
||||
import {
|
||||
parseRemoteDesktopDefaults,
|
||||
parseTerminalDefaults,
|
||||
type RemoteDesktopDefaults,
|
||||
type TerminalDefaults,
|
||||
} from "@/lib/connection-defaults";
|
||||
|
||||
interface ConnectionDefaultsContextValue {
|
||||
ready: boolean;
|
||||
terminal: TerminalDefaults;
|
||||
rdp: RemoteDesktopDefaults;
|
||||
saveTerminalDefaults: (value: TerminalDefaults) => Promise<void>;
|
||||
saveRdpDefaults: (value: RemoteDesktopDefaults) => Promise<void>;
|
||||
}
|
||||
|
||||
const ConnectionDefaultsContext = createContext<ConnectionDefaultsContextValue>(
|
||||
{
|
||||
ready: true,
|
||||
terminal: {},
|
||||
rdp: {},
|
||||
saveTerminalDefaults: async () => {},
|
||||
saveRdpDefaults: async () => {},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -41,7 +35,6 @@ export function ConnectionDefaultsProvider({
|
||||
}) {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [terminal, setTerminal] = useState<TerminalDefaults>({});
|
||||
const [rdp, setRdp] = useState<RemoteDesktopDefaults>({});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -49,7 +42,6 @@ export function ConnectionDefaultsProvider({
|
||||
.then((preferences) => {
|
||||
if (cancelled) return;
|
||||
setTerminal(parseTerminalDefaults(preferences.terminalDefaults));
|
||||
setRdp(parseRemoteDesktopDefaults(preferences.rdpDefaults));
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
@@ -64,14 +56,10 @@ export function ConnectionDefaultsProvider({
|
||||
await saveUserPreferences({ terminalDefaults: JSON.stringify(value) });
|
||||
setTerminal(value);
|
||||
}, []);
|
||||
const saveRdpDefaults = useCallback(async (value: RemoteDesktopDefaults) => {
|
||||
await saveUserPreferences({ rdpDefaults: JSON.stringify(value) });
|
||||
setRdp(value);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ ready, terminal, rdp, saveTerminalDefaults, saveRdpDefaults }),
|
||||
[ready, terminal, rdp, saveTerminalDefaults, saveRdpDefaults],
|
||||
() => ({ ready, terminal, saveTerminalDefaults }),
|
||||
[ready, terminal, saveTerminalDefaults],
|
||||
);
|
||||
return (
|
||||
<ConnectionDefaultsContext.Provider value={value}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { enabledHostProtocols, protocolPort } from "@/sidebar/host-protocols";
|
||||
import { useSshAuthProviders } from "@/hooks/useSshAuthProviders";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { Button } from "@/components/button";
|
||||
@@ -316,15 +317,12 @@ function QuickActionsCard({
|
||||
const { t } = useTranslation();
|
||||
const pinnedHosts = hosts.filter((h) => h.pin);
|
||||
const getConnectionEndpoint = (host: Host) => {
|
||||
const protocol = host.enableSsh ? undefined : enabledHostProtocols(host)[0];
|
||||
const port = host.enableSsh
|
||||
? host.sshPort
|
||||
: host.enableRdp
|
||||
? host.rdpPort
|
||||
: host.enableVnc
|
||||
? host.vncPort
|
||||
: host.enableTelnet
|
||||
? host.telnetPort
|
||||
: host.port;
|
||||
: protocol
|
||||
? protocolPort(host.pluginSettings, protocol)
|
||||
: host.port;
|
||||
return `${host.ip}:${port ?? host.port}`;
|
||||
};
|
||||
const renderConnectionIcon = (host: Host) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { GuacamoleConfig } from "@/types/guacamole-config";
|
||||
import type { TerminalConfig } from "@/types";
|
||||
|
||||
export type TerminalDefaults = Partial<
|
||||
@@ -20,8 +19,6 @@ export type TerminalDefaults = Partial<
|
||||
>
|
||||
>;
|
||||
|
||||
export type RemoteDesktopDefaults = Partial<GuacamoleConfig>;
|
||||
|
||||
function parseObject<T extends object>(value?: string | null): Partial<T> {
|
||||
if (!value) return {};
|
||||
try {
|
||||
@@ -38,12 +35,6 @@ export function parseTerminalDefaults(value?: string | null): TerminalDefaults {
|
||||
return parseObject<TerminalDefaults>(value);
|
||||
}
|
||||
|
||||
export function parseRemoteDesktopDefaults(
|
||||
value?: string | null,
|
||||
): RemoteDesktopDefaults {
|
||||
return parseObject<RemoteDesktopDefaults>(value);
|
||||
}
|
||||
|
||||
// Booleans have three states in the defaults UI: unset means the host decides.
|
||||
export type TriState = "inherit" | "on" | "off";
|
||||
|
||||
|
||||
@@ -4,12 +4,9 @@ import { websocketAuthProtocols } from "@/lib/ws-auth";
|
||||
export type ConnectionOrigin = "local" | "remote";
|
||||
|
||||
interface OriginResolvableHost {
|
||||
connectionType?: string | null;
|
||||
connectionOrigin?: ConnectionOrigin | null;
|
||||
}
|
||||
|
||||
const GUACAMOLE_CONNECTION_TYPES = new Set(["rdp", "vnc", "telnet"]);
|
||||
|
||||
/**
|
||||
* Resolves which backend a given host's interactive connection (SSH, Docker
|
||||
* console, RDP/VNC/Telnet) should dial: the desktop app's embedded local
|
||||
@@ -21,16 +18,16 @@ const GUACAMOLE_CONNECTION_TYPES = new Set(["rdp", "vnc", "telnet"]);
|
||||
* Everything else follows the host's own override if set, falling back to
|
||||
* the desktop-wide default.
|
||||
*
|
||||
* RDP/VNC/Telnet are the exception to that fallback: left on Default they
|
||||
* resolve to "remote" rather than following the desktop-wide setting. They
|
||||
* need a guacd, which the desktop does not ship, so originating them here
|
||||
* only works once the user has pointed Termix at one of their own (the
|
||||
* global guacd URL setting, or a host's guacd Proxy override). Making that
|
||||
* opt-in per host keeps an upgrade from moving working connections onto a
|
||||
* guacd that isn't there -- see Termix-SSH/Support#1240.
|
||||
* `defaultRemote` makes a host left on Default resolve to "remote" instead of
|
||||
* following the desktop-wide setting. Remote desktop passes it: it needs a
|
||||
* guacd, which the desktop does not ship, so originating it here only works
|
||||
* once the user has pointed Termix at one of their own. Making that opt-in
|
||||
* per host keeps an upgrade from moving working connections onto a guacd
|
||||
* that isn't there -- see Termix-SSH/Support#1240.
|
||||
*/
|
||||
export async function resolveConnectionOrigin(
|
||||
host: OriginResolvableHost,
|
||||
options: { defaultRemote?: boolean } = {},
|
||||
): Promise<ConnectionOrigin> {
|
||||
if (!isElectron()) {
|
||||
return "local";
|
||||
@@ -38,7 +35,7 @@ export async function resolveConnectionOrigin(
|
||||
if (host.connectionOrigin === "local" || host.connectionOrigin === "remote") {
|
||||
return host.connectionOrigin;
|
||||
}
|
||||
if (GUACAMOLE_CONNECTION_TYPES.has(host.connectionType ?? "")) {
|
||||
if (options.defaultRemote) {
|
||||
return "remote";
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
import type { AxiosInstance } from "axios";
|
||||
import { authApi } from "@/main-axios";
|
||||
import { authApi, createRemoteOriginApiInstance } from "@/main-axios";
|
||||
import { getBasePath } from "@/lib/base-path";
|
||||
import { isElectron } from "@/lib/electron";
|
||||
import { websocketAuthProtocols } from "@/lib/ws-auth";
|
||||
@@ -75,6 +75,27 @@ export function createPluginApi(pluginId: string): AxiosInstance {
|
||||
}) as AxiosInstance;
|
||||
}
|
||||
|
||||
const remotePluginApis = new Map<string, AxiosInstance>();
|
||||
|
||||
/**
|
||||
* The same client pointed at the connected remote server, for a desktop app
|
||||
* host whose connection origin is "remote". Outside Electron, and for
|
||||
* "local", it is the ordinary client.
|
||||
*/
|
||||
export function pluginApiFor(
|
||||
pluginId: string,
|
||||
origin: ConnectionOrigin | undefined,
|
||||
local: AxiosInstance,
|
||||
): AxiosInstance {
|
||||
if (origin !== "remote" || !isElectron()) return local;
|
||||
let client = remotePluginApis.get(pluginId);
|
||||
if (!client) {
|
||||
client = createRemoteOriginApiInstance(pluginApiPath(pluginId));
|
||||
remotePluginApis.set(pluginId, client);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* pluginWsUrl for a path a backend handed out ("/plugin-ws/<id>/<path>?q"),
|
||||
* so a caller can dial a plugin's socket without naming the plugin.
|
||||
|
||||
+2
-26
@@ -725,7 +725,6 @@
|
||||
"connectionDetails": "Connection Details",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Remote Desktop",
|
||||
"port": "Port",
|
||||
"username": "Username",
|
||||
"folder": "Folder",
|
||||
@@ -752,10 +751,7 @@
|
||||
"tabGeneral": "General",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Host Metrics",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Sharing",
|
||||
"tabAuthentication": "Authentication",
|
||||
"terminal": "Terminal",
|
||||
@@ -829,8 +825,6 @@
|
||||
"deselectAll": "Deselect All",
|
||||
"protocols": "Protocols",
|
||||
"secureShell": "Secure Shell",
|
||||
"virtualNetwork": "Virtual Network",
|
||||
"unencryptedShell": "Unencrypted shell",
|
||||
"addressIp": "Address / IP",
|
||||
"friendlyName": "Friendly Name",
|
||||
"macAddress": "MAC Address",
|
||||
@@ -853,7 +847,6 @@
|
||||
"useSocks5ProxyDesc": "Route connection through a proxy server",
|
||||
"connectionOrigin": "Connection Origin",
|
||||
"connectionOriginDesc": "Where this host's connection originates from. Overrides the desktop app's global default.",
|
||||
"connectionOriginRemoteDesktopNote": "RDP, VNC and Telnet use the remote server unless you pick \"This device\", which needs a guacd this machine can reach — set one in the guacd URL setting, or per host under guacd Proxy.",
|
||||
"connectionOriginDefault": "Use default",
|
||||
"connectionOriginLocal": "This device (local network)",
|
||||
"connectionOriginRemote": "Remote server",
|
||||
@@ -1378,9 +1371,6 @@
|
||||
"filterAuthStepca": "Step CA",
|
||||
"filterProtocolGroup": "Protocol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Features",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "File Manager",
|
||||
@@ -1478,8 +1468,6 @@
|
||||
"authTypeDirect": "Direct",
|
||||
"authTypeCredential": "Credential",
|
||||
"authTypeNone": "None",
|
||||
"colorDepth": "Color Depth",
|
||||
"resizeMethod": "Resize Method",
|
||||
"recordingPath": "Recording Path",
|
||||
"recordingName": "Recording Name",
|
||||
"saveHostFirst": "Save the host first.",
|
||||
@@ -2117,10 +2105,6 @@
|
||||
"metrics": "Metrics",
|
||||
"sec": "sec",
|
||||
"logLevel": "Log Level",
|
||||
"enableRemoteDesktop": "Enable Remote Desktop",
|
||||
"enableRemoteDesktopDesc": "RDP/VNC remote desktop",
|
||||
"enableRemoteDesktopDocsLink": "View docs",
|
||||
"guacdUrl": "guacd URL",
|
||||
"oidcDescription": "Configure OpenID Connect for SSO. Fields marked * are required.",
|
||||
"oidcDocsLink": "View docs",
|
||||
"oidcClientId": "Client ID",
|
||||
@@ -2266,9 +2250,6 @@
|
||||
"monitoringIntervalInvalid": "Invalid interval values",
|
||||
"monitoringSaved": "Monitoring settings saved",
|
||||
"monitoringSaveFailed": "Failed to save monitoring settings",
|
||||
"remoteDesktopSaved": "Remote Desktop settings saved",
|
||||
"remoteDesktopSaveFailed": "Failed to save Remote Desktop settings",
|
||||
"remoteDesktopUpdateFailed": "Failed to update Remote Desktop setting",
|
||||
"logLevelUpdateFailed": "Failed to update log level",
|
||||
"oidcSaved": "OIDC configuration saved",
|
||||
"oidcSaveFailed": "Failed to save OIDC config",
|
||||
@@ -2457,10 +2438,9 @@
|
||||
"protocolLabel": "Protocol",
|
||||
"domainLabel": "Domain",
|
||||
"domainPlaceholder": "optional",
|
||||
"connectToRdp": "Connect via RDP",
|
||||
"connectToVnc": "Connect via VNC",
|
||||
"connectToTerminal": "Connect to Terminal",
|
||||
"connectToFiles": "Connect to Files"
|
||||
"connectToFiles": "Connect to Files",
|
||||
"connectWith": "Connect via {{protocol}}"
|
||||
},
|
||||
"sshTools": {
|
||||
"keyRecordingTitle": "Key Recording",
|
||||
@@ -2667,13 +2647,9 @@
|
||||
"description": "Set values that every host inherits unless it overrides them.",
|
||||
"manage": "Manage",
|
||||
"dialogDescription": "These apply to hosts that leave a setting unset. Anything set on a host wins.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Remote Desktop",
|
||||
"inherit": "Inherit",
|
||||
"on": "On",
|
||||
"off": "Off",
|
||||
"resizeDisplayUpdate": "Display Update",
|
||||
"resizeReconnect": "Reconnect",
|
||||
"clearAll": "Clear All",
|
||||
"saved": "Connection defaults saved",
|
||||
"saveFailed": "Failed to save connection defaults",
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Verbindingsbesonderhede",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Afstandslessenaar",
|
||||
"port": "Hawe",
|
||||
"username": "Gebruikersnaam",
|
||||
"folder": "Vouer",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Algemeen",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminaal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Gasheermetrieke",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Deling",
|
||||
"tabAuthentication": "Verifikasie",
|
||||
"terminal": "Terminaal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Deselekteer alles",
|
||||
"protocols": "Protokolle",
|
||||
"secureShell": "Veilige Dop",
|
||||
"virtualNetwork": "Virtuele Netwerk",
|
||||
"unencryptedShell": "Ongeënkripteerde dop",
|
||||
"addressIp": "Adres / IP",
|
||||
"friendlyName": "Vriendelike Naam",
|
||||
"macAddress": "MAC-adres",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Stap CA",
|
||||
"filterProtocolGroup": "Protokol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Kenmerke",
|
||||
"filterFeatureTerminal": "Terminaal",
|
||||
"filterFeatureFileManager": "Lêerbestuurder",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Direkte",
|
||||
"authTypeCredential": "Geloofsbrief",
|
||||
"authTypeNone": "Geen",
|
||||
"colorDepth": "Kleurdiepte",
|
||||
"resizeMethod": "Vergrootingsmetode",
|
||||
"recordingPath": "Opnamepad",
|
||||
"recordingName": "Opnamenaam",
|
||||
"saveHostFirst": "Stoor eers die gasheer.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Aktiveer Guacamole",
|
||||
"enableGuacamoleDesc": "RDP/VNC-afstandlessenaar",
|
||||
"enableGuacamoleDocsLink": "Bekyk dokumente",
|
||||
"guacdUrl": "guacd-URL",
|
||||
"tailscaleApiKey": "Tailscale API-sleutel",
|
||||
"tailscaleApiKeyDescription": "Word gebruik vir toestelontdekking in die gasheerredigeerder. Ondersteun Tailscale (tskey-) en Headscale (hskey-) sleutels. Genereer 'n sleutel by tailscale.com/admin/settings/keys of vanaf jou Headscale-instansie.",
|
||||
"tailscaleApiKeyDocsLink": "Bekyk dokumente",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protokol",
|
||||
"domainLabel": "Domein",
|
||||
"domainPlaceholder": "opsioneel",
|
||||
"connectToRdp": "Verbind via RDP",
|
||||
"connectToVnc": "Verbind via VNC",
|
||||
"connectToTerminal": "Koppel aan terminaal",
|
||||
"connectToFiles": "Koppel aan lêers"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Stel waardes wat elke gasheer erf tensy dit hulle oorskryf.",
|
||||
"manage": "Bestuur",
|
||||
"dialogDescription": "Dit geld vir gashere wat 'n instelling ongestel laat. Enigiets wat op 'n gasheer gestel word, wen.",
|
||||
"tabTerminal": "Terminaal",
|
||||
"tabRemoteDesktop": "Afstandslessenaar",
|
||||
"inherit": "Erf",
|
||||
"on": "Aan",
|
||||
"off": "Af",
|
||||
"resizeDisplayUpdate": "Skermopdatering",
|
||||
"resizeReconnect": "Herkoppel",
|
||||
"clearAll": "Vee alles uit",
|
||||
"saved": "Verbindingsverstekke gestoor",
|
||||
"saveFailed": "Kon nie verbindingsverstekinstellings stoor nie"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "تفاصيل الاتصال",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "سطح المكتب البعيد",
|
||||
"port": "المنفذ",
|
||||
"username": "اسم المستخدم",
|
||||
"folder": "المجلد",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "عام",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "الطرفية",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "مقاييس المضيف",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "المشاركة",
|
||||
"tabAuthentication": "المصادقة",
|
||||
"terminal": "الطرفية",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "إلغاء تحديد الكل",
|
||||
"protocols": "البروتوكولات",
|
||||
"secureShell": "Secure Shell",
|
||||
"virtualNetwork": "الشبكة الافتراضية",
|
||||
"unencryptedShell": "shell غير مشفر",
|
||||
"addressIp": "العنوان / IP",
|
||||
"friendlyName": "الاسم المألوف",
|
||||
"macAddress": "عنوان MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "الخطوة ج",
|
||||
"filterProtocolGroup": "البروتوكول",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "الميزات",
|
||||
"filterFeatureTerminal": "الطرفية",
|
||||
"filterFeatureFileManager": "مدير الملفات",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "مباشر",
|
||||
"authTypeCredential": "بيانات الاعتماد",
|
||||
"authTypeNone": "لا أحد",
|
||||
"colorDepth": "عمق اللون",
|
||||
"resizeMethod": "طريقة تغيير الحجم",
|
||||
"recordingPath": "مسار التسجيل",
|
||||
"recordingName": "اسم التسجيل",
|
||||
"saveHostFirst": "احفظ المضيف أولاً.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "تمكين Guacamole",
|
||||
"enableGuacamoleDesc": "سطح المكتب البعيد RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "عرض المستندات",
|
||||
"guacdUrl": "رابط guacd",
|
||||
"tailscaleApiKey": "مفتاح Tailscale API",
|
||||
"tailscaleApiKeyDescription": "يُستخدم لاكتشاف الأجهزة في محرر المضيف. يدعم مفاتيح Tailscale (tskey-) وHeadscale (hskey-). أنشئ مفتاحًا من خلال tailscale.com/admin/settings/keys أو من خلال حسابك في Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "عرض المستندات",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "بروتوكول",
|
||||
"domainLabel": "اِختِصاص",
|
||||
"domainPlaceholder": "خياري",
|
||||
"connectToRdp": "الاتصال عبر بروتوكول سطح المكتب البعيد (RDP)",
|
||||
"connectToVnc": "الاتصال عبر VNC",
|
||||
"connectToTerminal": "اتصل بالطرفية",
|
||||
"connectToFiles": "اتصل بالملفات"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "قم بتعيين القيم التي يرثها كل مضيف ما لم يقم بتجاوزها.",
|
||||
"manage": "يدير",
|
||||
"dialogDescription": "تنطبق هذه الشروط على الأجهزة المضيفة التي تترك إعدادًا غير مُحدد. أي إعداد مُحدد على الجهاز المضيف هو الذي يُعتمد.",
|
||||
"tabTerminal": "صالة",
|
||||
"tabRemoteDesktop": "سطح المكتب البعيد",
|
||||
"inherit": "يرث",
|
||||
"on": "على",
|
||||
"off": "عن",
|
||||
"resizeDisplayUpdate": "تحديث الشاشة",
|
||||
"resizeReconnect": "إعادة الاتصال",
|
||||
"clearAll": "مسح الكل",
|
||||
"saved": "تم حفظ إعدادات الاتصال الافتراضية",
|
||||
"saveFailed": "فشل حفظ إعدادات الاتصال الافتراضية"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Детайли за връзката",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Телнет",
|
||||
"remoteDesktop": "Отдалечен работен плот",
|
||||
"port": "Порт",
|
||||
"username": "Потребителско име",
|
||||
"folder": "Папка",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Общи",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Терминал",
|
||||
"tabRdp": "ПРСР",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Метрики на хоста",
|
||||
"tabTelnet": "Телнет",
|
||||
"tabSharing": "Споделяне",
|
||||
"tabAuthentication": "Удостоверяване",
|
||||
"terminal": "Терминал",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Премахване на избора от всички",
|
||||
"protocols": "Протоколи",
|
||||
"secureShell": "Защитена обвивка",
|
||||
"virtualNetwork": "Виртуална мрежа",
|
||||
"unencryptedShell": "Некриптирана обвивка",
|
||||
"addressIp": "Адрес / IP адрес",
|
||||
"friendlyName": "Приятелско име",
|
||||
"macAddress": "MAC адрес",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Стъпка CA",
|
||||
"filterProtocolGroup": "Протокол",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "ПРСР",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Телнет",
|
||||
"filterFeaturesGroup": "Характеристики",
|
||||
"filterFeatureTerminal": "Терминал",
|
||||
"filterFeatureFileManager": "Файлов мениджър",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Директно",
|
||||
"authTypeCredential": "Пълномощия",
|
||||
"authTypeNone": "Няма",
|
||||
"colorDepth": "Дълбочина на цвета",
|
||||
"resizeMethod": "Метод за преоразмеряване",
|
||||
"recordingPath": "Път на запис",
|
||||
"recordingName": "Име на записа",
|
||||
"saveHostFirst": "Първо запазете хоста.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Активиране на гуакамоле",
|
||||
"enableGuacamoleDesc": "RDP/VNC отдалечен работен плот",
|
||||
"enableGuacamoleDocsLink": "Преглед на документи",
|
||||
"guacdUrl": "URL адрес на GUACD",
|
||||
"tailscaleApiKey": "API ключ на Tailscale",
|
||||
"tailscaleApiKeyDescription": "Използва се за откриване на устройства в редактора на хостове. Поддържа ключове Tailscale (tkey-) и Headscale (hskey-). Генерирайте ключ на tailscale.com/admin/settings/keys или от вашия Headscale екземпляр.",
|
||||
"tailscaleApiKeyDocsLink": "Преглед на документи",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Протокол",
|
||||
"domainLabel": "Домейн",
|
||||
"domainPlaceholder": "по избор",
|
||||
"connectToRdp": "Свързване чрез RDP",
|
||||
"connectToVnc": "Свързване чрез VNC",
|
||||
"connectToTerminal": "Свързване с терминала",
|
||||
"connectToFiles": "Свързване с файлове"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Задайте стойности, които всеки хост наследява, освен ако не ги презапише.",
|
||||
"manage": "Управление",
|
||||
"dialogDescription": "Това се отнася за хостове, които оставят настройка незададена. Всичко, което е зададено на хост, печели.",
|
||||
"tabTerminal": "Терминал",
|
||||
"tabRemoteDesktop": "Отдалечен работен плот",
|
||||
"inherit": "Наследяване",
|
||||
"on": "Включено",
|
||||
"off": "Изключено",
|
||||
"resizeDisplayUpdate": "Актуализация на дисплея",
|
||||
"resizeReconnect": "Свържете се отново",
|
||||
"clearAll": "Изчисти всички",
|
||||
"saved": "Настройките по подразбиране на връзката са запазени",
|
||||
"saveFailed": "Запазването на настройките по подразбиране за връзка не бе успешно"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "সংযোগের বিবরণ",
|
||||
"ssh": "এসএসএইচ",
|
||||
"telnet": "টেলনেট",
|
||||
"remoteDesktop": "রিমোট ডেস্কটপ",
|
||||
"port": "বন্দর",
|
||||
"username": "ব্যবহারকারীর নাম",
|
||||
"folder": "ফোল্ডার",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "সাধারণ",
|
||||
"tabSsh": "এসএসএইচ",
|
||||
"tabTerminal": "টার্মিনাল",
|
||||
"tabRdp": "আরডিপি",
|
||||
"tabVnc": "ভিএনসি",
|
||||
"tabStats": "হোস্ট মেট্রিক্স",
|
||||
"tabTelnet": "টেলনেট",
|
||||
"tabSharing": "শেয়ারিং",
|
||||
"tabAuthentication": "প্রমাণীকরণ",
|
||||
"terminal": "টার্মিনাল",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "সবগুলো নির্বাচন বাতিল করুন",
|
||||
"protocols": "প্রোটোকল",
|
||||
"secureShell": "সুরক্ষিত খোলস",
|
||||
"virtualNetwork": "ভার্চুয়াল নেটওয়ার্ক",
|
||||
"unencryptedShell": "এনক্রিপ্টবিহীন শেল",
|
||||
"addressIp": "ঠিকানা / আইপি",
|
||||
"friendlyName": "বন্ধুত্বপূর্ণ নাম",
|
||||
"macAddress": "MAC ঠিকানা",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "স্টেপ সিএ",
|
||||
"filterProtocolGroup": "প্রোটোকল",
|
||||
"filterProtocolSsh": "এসএসএইচ",
|
||||
"filterProtocolRdp": "আরডিপি",
|
||||
"filterProtocolVnc": "ভিএনসি",
|
||||
"filterProtocolTelnet": "টেলনেট",
|
||||
"filterFeaturesGroup": "বৈশিষ্ট্য",
|
||||
"filterFeatureTerminal": "টার্মিনাল",
|
||||
"filterFeatureFileManager": "ফাইল ম্যানেজার",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "সরাসরি",
|
||||
"authTypeCredential": "শংসাপত্র",
|
||||
"authTypeNone": "কোনোটিই না",
|
||||
"colorDepth": "রঙের গভীরতা",
|
||||
"resizeMethod": "আকার পরিবর্তন পদ্ধতি",
|
||||
"recordingPath": "রেকর্ডিং পথ",
|
||||
"recordingName": "রেকর্ডিং নাম",
|
||||
"saveHostFirst": "প্রথমে হোস্টটি সংরক্ষণ করুন।",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "গুয়াকামোল সক্রিয় করুন",
|
||||
"enableGuacamoleDesc": "RDP/VNC রিমোট ডেস্কটপ",
|
||||
"enableGuacamoleDocsLink": "নথি দেখুন",
|
||||
"guacdUrl": "guacd URL",
|
||||
"tailscaleApiKey": "টেইলস্কেল এপিআই কী",
|
||||
"tailscaleApiKeyDescription": "হোস্ট এডিটরে ডিভাইস শনাক্তকরণের জন্য ব্যবহৃত হয়। এটি টেইলস্কেল (tskey-) এবং হেডস্কেল (hskey-) কী সমর্থন করে। tailscale.com/admin/settings/keys থেকে অথবা আপনার হেডস্কেল ইনস্ট্যান্স থেকে একটি কী তৈরি করুন।",
|
||||
"tailscaleApiKeyDocsLink": "নথি দেখুন",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "প্রোটোকল",
|
||||
"domainLabel": "ডোমেইন",
|
||||
"domainPlaceholder": "ঐচ্ছিক",
|
||||
"connectToRdp": "RDP এর মাধ্যমে সংযোগ করুন",
|
||||
"connectToVnc": "VNC এর মাধ্যমে সংযোগ করুন",
|
||||
"connectToTerminal": "টার্মিনালের সাথে সংযোগ করুন",
|
||||
"connectToFiles": "ফাইলগুলির সাথে সংযোগ করুন"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "এমন মান নির্ধারণ করুন যা প্রতিটি হোস্ট উত্তরাধিকারসূত্রে পাবে, যদি না সে সেগুলোকে অগ্রাহ্য করে।",
|
||||
"manage": "পরিচালনা করুন",
|
||||
"dialogDescription": "এই নিয়মগুলো সেইসব হোস্টের ক্ষেত্রে প্রযোজ্য যারা কোনো সেটিং সেট করে না। হোস্টে যা কিছু সেট করা থাকে, তা-ই কার্যকর হয়।",
|
||||
"tabTerminal": "টার্মিনাল",
|
||||
"tabRemoteDesktop": "রিমোট ডেস্কটপ",
|
||||
"inherit": "উত্তরাধিকার সূত্রে",
|
||||
"on": "চালু",
|
||||
"off": "বন্ধ",
|
||||
"resizeDisplayUpdate": "ডিসপ্লে আপডেট",
|
||||
"resizeReconnect": "পুনরায় সংযোগ করুন",
|
||||
"clearAll": "সব পরিষ্কার করুন",
|
||||
"saved": "সংযোগের ডিফল্ট মান সংরক্ষিত আছে",
|
||||
"saveFailed": "সংযোগের ডিফল্ট মান সংরক্ষণ করতে ব্যর্থ হয়েছে"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Detalls de la connexió",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Escriptori remot",
|
||||
"port": "Port",
|
||||
"username": "Nom d'usuari",
|
||||
"folder": "Carpeta",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "General",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Mètriques de l'amfitrió",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Compartir",
|
||||
"tabAuthentication": "Autenticació",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Desselecciona-ho tot",
|
||||
"protocols": "Protocols",
|
||||
"secureShell": "Shell segur",
|
||||
"virtualNetwork": "Xarxa virtual",
|
||||
"unencryptedShell": "Shell sense xifrar",
|
||||
"addressIp": "Adreça / IP",
|
||||
"friendlyName": "Nom amigable",
|
||||
"macAddress": "Adreça MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Pas CA",
|
||||
"filterProtocolGroup": "Protocol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Característiques",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Gestor de fitxers",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Directe",
|
||||
"authTypeCredential": "Credencial",
|
||||
"authTypeNone": "Cap",
|
||||
"colorDepth": "Profunditat de color",
|
||||
"resizeMethod": "Mètode de redimensionament",
|
||||
"recordingPath": "Ruta d'enregistrament",
|
||||
"recordingName": "Nom de l'enregistrament",
|
||||
"saveHostFirst": "Primer desa l'amfitrió.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Activa el guacamole",
|
||||
"enableGuacamoleDesc": "Escriptori remot RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Veure documents",
|
||||
"guacdUrl": "URL de guacd",
|
||||
"tailscaleApiKey": "Clau API de Tailscale",
|
||||
"tailscaleApiKeyDescription": "S'utilitza per a la detecció de dispositius a l'editor de l'amfitrió. Admet les claus Tailscale (tskey-) i Headscale (hskey-). Genereu una clau a tailscale.com/admin/settings/keys o des de la vostra instància de Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Veure documents",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protocol",
|
||||
"domainLabel": "Domini",
|
||||
"domainPlaceholder": "opcional",
|
||||
"connectToRdp": "Connecta't via RDP",
|
||||
"connectToVnc": "Connecta't mitjançant VNC",
|
||||
"connectToTerminal": "Connecta't al terminal",
|
||||
"connectToFiles": "Connecta't a fitxers"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Defineix els valors que cada amfitrió hereta tret que els sobreescrigui.",
|
||||
"manage": "Gestiona",
|
||||
"dialogDescription": "Això s'aplica als amfitrions que deixen una configuració sense definir. Qualsevol cosa definida en un amfitrió guanya.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Escriptori remot",
|
||||
"inherit": "Heretar",
|
||||
"on": "Activat",
|
||||
"off": "Desactivat",
|
||||
"resizeDisplayUpdate": "Actualització de la pantalla",
|
||||
"resizeReconnect": "Reconnecta",
|
||||
"clearAll": "Esborra-ho tot",
|
||||
"saved": "Els valors predeterminats de connexió s'han desat",
|
||||
"saveFailed": "No s'han pogut desar els valors predeterminats de la connexió"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Podrobnosti o připojení",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Vzdálená plocha",
|
||||
"port": "Přístav",
|
||||
"username": "Uživatelské jméno",
|
||||
"folder": "Složka",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Generál",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminál",
|
||||
"tabRdp": "PRV",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Metriky hostitele",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Sdílení",
|
||||
"tabAuthentication": "Ověřování",
|
||||
"terminal": "Terminál",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Zrušit výběr všeho",
|
||||
"protocols": "Protokoly",
|
||||
"secureShell": "Bezpečné prostředí",
|
||||
"virtualNetwork": "Virtuální síť",
|
||||
"unencryptedShell": "Nešifrovaný shell",
|
||||
"addressIp": "Adresa / IP adresa",
|
||||
"friendlyName": "Přátelský název",
|
||||
"macAddress": "MAC adresa",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Krok CA",
|
||||
"filterProtocolGroup": "Protokol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "PRV",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funkce",
|
||||
"filterFeatureTerminal": "Terminál",
|
||||
"filterFeatureFileManager": "Správce souborů",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Řídit",
|
||||
"authTypeCredential": "Pověření",
|
||||
"authTypeNone": "Žádný",
|
||||
"colorDepth": "Barevná hloubka",
|
||||
"resizeMethod": "Metoda změny velikosti",
|
||||
"recordingPath": "Cesta záznamu",
|
||||
"recordingName": "Název nahrávky",
|
||||
"saveHostFirst": "Nejprve uložte hostitele.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Povolit guacamole",
|
||||
"enableGuacamoleDesc": "Vzdálená plocha RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Zobrazit dokumenty",
|
||||
"guacdUrl": "URL GUACD",
|
||||
"tailscaleApiKey": "Klíč API Tailscale",
|
||||
"tailscaleApiKeyDescription": "Používá se pro vyhledávání zařízení v editoru hostitele. Podporuje klíče Tailscale (tkey-) a Headscale (hskey-). Klíč si můžete vygenerovat na adrese tailscale.com/admin/settings/keys nebo z vaší instance Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Zobrazit dokumenty",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protokol",
|
||||
"domainLabel": "Doména",
|
||||
"domainPlaceholder": "volitelný",
|
||||
"connectToRdp": "Připojení přes RDP",
|
||||
"connectToVnc": "Připojení přes VNC",
|
||||
"connectToTerminal": "Připojení k terminálu",
|
||||
"connectToFiles": "Připojení k souborům"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Nastavte hodnoty, které zdědí každý hostitel, pokud je nepřepíše.",
|
||||
"manage": "Spravovat",
|
||||
"dialogDescription": "Toto platí pro hostitele, kteří nechají nastavení nenastavené. Cokoli, co je na hostiteli nastaveno, vyhrává.",
|
||||
"tabTerminal": "Terminál",
|
||||
"tabRemoteDesktop": "Vzdálená plocha",
|
||||
"inherit": "Zdědit",
|
||||
"on": "Na",
|
||||
"off": "Vypnuto",
|
||||
"resizeDisplayUpdate": "Aktualizace displeje",
|
||||
"resizeReconnect": "Znovu se připojte",
|
||||
"clearAll": "Vymazat vše",
|
||||
"saved": "Výchozí nastavení připojení uloženo",
|
||||
"saveFailed": "Nepodařilo se uložit výchozí nastavení připojení"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Forbindelsesdetaljer",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Fjernskrivebord",
|
||||
"port": "Havn",
|
||||
"username": "Brugernavn",
|
||||
"folder": "Folder",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Generel",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "Landdistriktsudviklingsplan",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Værtsmålinger",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Deling",
|
||||
"tabAuthentication": "Godkendelse",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Fravælg alle",
|
||||
"protocols": "Protokoller",
|
||||
"secureShell": "Sikker skal",
|
||||
"virtualNetwork": "Virtuelt netværk",
|
||||
"unencryptedShell": "Ukrypteret skal",
|
||||
"addressIp": "Adresse / IP",
|
||||
"friendlyName": "Venligt navn",
|
||||
"macAddress": "MAC-adresse",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Trin CA",
|
||||
"filterProtocolGroup": "Protokol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "Landdistriktsudviklingsplan",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funktioner",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Filhåndtering",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Direkte",
|
||||
"authTypeCredential": "Legitimationsoplysninger",
|
||||
"authTypeNone": "Ingen",
|
||||
"colorDepth": "Farvedybde",
|
||||
"resizeMethod": "Ændre størrelse-metode",
|
||||
"recordingPath": "Optagelsessti",
|
||||
"recordingName": "Optagelsesnavn",
|
||||
"saveHostFirst": "Gem værten først.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Aktivér guacamole",
|
||||
"enableGuacamoleDesc": "RDP/VNC fjernskrivebord",
|
||||
"enableGuacamoleDocsLink": "Se dokumenter",
|
||||
"guacdUrl": "guacd-URL",
|
||||
"tailscaleApiKey": "Tailscale API-nøgle",
|
||||
"tailscaleApiKeyDescription": "Bruges til enhedsregistrering i værtseditoren. Understøtter Tailscale (tskey-) og Headscale (hskey-) nøgler. Generer en nøgle på tailscale.com/admin/settings/keys eller fra din Headscale-instans.",
|
||||
"tailscaleApiKeyDocsLink": "Se dokumenter",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protokol",
|
||||
"domainLabel": "Domæne",
|
||||
"domainPlaceholder": "valgfri",
|
||||
"connectToRdp": "Opret forbindelse via RDP",
|
||||
"connectToVnc": "Forbind via VNC",
|
||||
"connectToTerminal": "Opret forbindelse til terminalen",
|
||||
"connectToFiles": "Opret forbindelse til filer"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Angiv værdier, som hver vært arver, medmindre den tilsidesætter dem.",
|
||||
"manage": "Styre",
|
||||
"dialogDescription": "Disse gælder for værter, der ikke har angivet en indstilling. Alt, hvad der er angivet på en vært, vinder.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Fjernskrivebord",
|
||||
"inherit": "Arve",
|
||||
"on": "På",
|
||||
"off": "Slukket",
|
||||
"resizeDisplayUpdate": "Opdatering af display",
|
||||
"resizeReconnect": "Genopret forbindelse",
|
||||
"clearAll": "Ryd alt",
|
||||
"saved": "Standardindstillinger for forbindelser gemt",
|
||||
"saveFailed": "Kunne ikke gemme standardforbindelsesindstillinger"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Verbindungsdetails",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Remotedesktop",
|
||||
"port": "Port",
|
||||
"username": "Benutzername",
|
||||
"folder": "Ordner",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Allgemein",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Host-Metriken",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Freigabe",
|
||||
"tabAuthentication": "Authentifizierung",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Auswahl aufheben",
|
||||
"protocols": "Protokolle",
|
||||
"secureShell": "Secure Shell",
|
||||
"virtualNetwork": "Virtuelles Netzwerk",
|
||||
"unencryptedShell": "Unverschlüsselte Shell",
|
||||
"addressIp": "Adresse / IP",
|
||||
"friendlyName": "Anzeigename",
|
||||
"macAddress": "MAC-Adresse",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Schritt CA",
|
||||
"filterProtocolGroup": "Protokoll",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funktionen",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Dateimanager",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Direkt",
|
||||
"authTypeCredential": "Anmeldeinformation",
|
||||
"authTypeNone": "Keiner",
|
||||
"colorDepth": "Farbtiefe",
|
||||
"resizeMethod": "Skalierungsmethode",
|
||||
"recordingPath": "Aufzeichnungspfad",
|
||||
"recordingName": "Aufzeichnungsname",
|
||||
"saveHostFirst": "Host zuerst speichern.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Guacamole aktivieren",
|
||||
"enableGuacamoleDesc": "RDP/VNC-Remotedesktop",
|
||||
"enableGuacamoleDocsLink": "Dokumentation anzeigen",
|
||||
"guacdUrl": "guacd-URL",
|
||||
"tailscaleApiKey": "Tailscale-API-Schlüssel",
|
||||
"tailscaleApiKeyDescription": "Wird zur Geräteerkennung im Host-Editor verwendet. Unterstützt Tailscale- (tskey-) und Headscale-Schlüssel (hskey-). Generieren Sie einen Schlüssel unter tailscale.com/admin/settings/keys oder in Ihrer Headscale-Instanz.",
|
||||
"tailscaleApiKeyDocsLink": "Dokumentation anzeigen",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protokoll",
|
||||
"domainLabel": "Domain",
|
||||
"domainPlaceholder": "optional",
|
||||
"connectToRdp": "Verbindung über RDP herstellen",
|
||||
"connectToVnc": "Verbindung über VNC herstellen",
|
||||
"connectToTerminal": "Mit Terminal verbinden",
|
||||
"connectToFiles": "Mit Dateien verbinden"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Werte festlegen, die jeder Host erbt, sofern er sie nicht überschreibt.",
|
||||
"manage": "Verwalten",
|
||||
"dialogDescription": "Diese Regelungen gelten für Hosts, die keine Einstellung vorgenommen haben. Alle auf einem Host festgelegten Einstellungen haben Vorrang.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Remote-Desktop",
|
||||
"inherit": "Erben",
|
||||
"on": "An",
|
||||
"off": "Aus",
|
||||
"resizeDisplayUpdate": "Anzeigeaktualisierung",
|
||||
"resizeReconnect": "Wiederverbinden",
|
||||
"clearAll": "Alles löschen",
|
||||
"saved": "Standardeinstellungen für die Verbindung gespeichert",
|
||||
"saveFailed": "Die Standardverbindungseinstellungen konnten nicht gespeichert werden."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Λεπτομέρειες σύνδεσης",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Τέλνετ",
|
||||
"remoteDesktop": "Απομακρυσμένη επιφάνεια εργασίας",
|
||||
"port": "Λιμάνι",
|
||||
"username": "Όνομα χρήστη",
|
||||
"folder": "Ντοσιέ",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Γενικός",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Τερματικό",
|
||||
"tabRdp": "ΠΑΑ",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Μετρήσεις κεντρικού υπολογιστή",
|
||||
"tabTelnet": "Τέλνετ",
|
||||
"tabSharing": "Μοιρασιά",
|
||||
"tabAuthentication": "Πιστοποίηση",
|
||||
"terminal": "Τερματικό",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Αποεπιλογή όλων",
|
||||
"protocols": "Πρωτόκολλα",
|
||||
"secureShell": "Ασφαλές κέλυφος",
|
||||
"virtualNetwork": "Εικονικό Δίκτυο",
|
||||
"unencryptedShell": "Μη κρυπτογραφημένο κέλυφος",
|
||||
"addressIp": "Διεύθυνση / IP",
|
||||
"friendlyName": "Φιλικό όνομα",
|
||||
"macAddress": "Διεύθυνση MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Βήμα CA",
|
||||
"filterProtocolGroup": "Πρωτόκολλο",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "ΠΑΑ",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Τέλνετ",
|
||||
"filterFeaturesGroup": "Χαρακτηριστικά",
|
||||
"filterFeatureTerminal": "Τερματικό",
|
||||
"filterFeatureFileManager": "Διαχειριστής αρχείων",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Απευθείας",
|
||||
"authTypeCredential": "Πιστοποιητικό",
|
||||
"authTypeNone": "Κανένας",
|
||||
"colorDepth": "Βάθος χρώματος",
|
||||
"resizeMethod": "Μέθοδος αλλαγής μεγέθους",
|
||||
"recordingPath": "Διαδρομή εγγραφής",
|
||||
"recordingName": "Όνομα εγγραφής",
|
||||
"saveHostFirst": "Αποθηκεύστε πρώτα τον οικοδεσπότη.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Ενεργοποίηση Γουακαμόλε",
|
||||
"enableGuacamoleDesc": "Απομακρυσμένη επιφάνεια εργασίας RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Προβολή εγγράφων",
|
||||
"guacdUrl": "URL guacd",
|
||||
"tailscaleApiKey": "Κλειδί API κλίμακας ουράς",
|
||||
"tailscaleApiKeyDescription": "Χρησιμοποιείται για την ανακάλυψη συσκευών στον επεξεργαστή κεντρικού υπολογιστή. Υποστηρίζει τα κλειδιά Tailscale (tskey-) και Headscale (hskey-). Δημιουργήστε ένα κλειδί στη διεύθυνση tailscale.com/admin/settings/keys ή από την παρουσία Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Προβολή εγγράφων",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Πρωτόκολλο",
|
||||
"domainLabel": "Πεδίο ορισμού",
|
||||
"domainPlaceholder": "προαιρετικός",
|
||||
"connectToRdp": "Σύνδεση μέσω RDP",
|
||||
"connectToVnc": "Σύνδεση μέσω VNC",
|
||||
"connectToTerminal": "Σύνδεση με τερματικό",
|
||||
"connectToFiles": "Σύνδεση με αρχεία"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Ορίστε τιμές που κληρονομεί κάθε κεντρικός υπολογιστής, εκτός εάν τις παρακάμπτει.",
|
||||
"manage": "Διαχειρίζομαι",
|
||||
"dialogDescription": "Αυτά ισχύουν για κεντρικούς υπολογιστές που αφήνουν μια ρύθμιση μη καθορισμένη. Οτιδήποτε οριστεί σε έναν κεντρικό υπολογιστή κερδίζει.",
|
||||
"tabTerminal": "Τερματικό",
|
||||
"tabRemoteDesktop": "Απομακρυσμένη επιφάνεια εργασίας",
|
||||
"inherit": "Κληρονομώ",
|
||||
"on": "Επί",
|
||||
"off": "Μακριά από",
|
||||
"resizeDisplayUpdate": "Ενημέρωση οθόνης",
|
||||
"resizeReconnect": "Επανασύνδεση",
|
||||
"clearAll": "Εκκαθάριση όλων",
|
||||
"saved": "Οι προεπιλογές σύνδεσης αποθηκεύτηκαν",
|
||||
"saveFailed": "Η αποθήκευση των προεπιλογών σύνδεσης απέτυχε"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Detalles de la conexión",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Escritorio remoto",
|
||||
"port": "Puerto",
|
||||
"username": "Nombre de usuario",
|
||||
"folder": "Carpeta",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "General",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Métricas del host",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Compartir",
|
||||
"tabAuthentication": "Autenticación",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Deseleccionar todo",
|
||||
"protocols": "Protocolos",
|
||||
"secureShell": "Shell seguro",
|
||||
"virtualNetwork": "Red virtual",
|
||||
"unencryptedShell": "Shell sin cifrar",
|
||||
"addressIp": "Dirección / IP",
|
||||
"friendlyName": "Nombre descriptivo",
|
||||
"macAddress": "Dirección MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Paso CA",
|
||||
"filterProtocolGroup": "Protocolo",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Características",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Gestor de archivos",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Directa",
|
||||
"authTypeCredential": "Credencial",
|
||||
"authTypeNone": "Ninguno",
|
||||
"colorDepth": "Profundidad de color",
|
||||
"resizeMethod": "Método de redimensionamiento",
|
||||
"recordingPath": "Ruta de grabación",
|
||||
"recordingName": "Nombre de la grabación",
|
||||
"saveHostFirst": "Primero guarde el host.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Activar Guacamole",
|
||||
"enableGuacamoleDesc": "Escritorio remoto RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Ver documentación",
|
||||
"guacdUrl": "URL de guacd",
|
||||
"tailscaleApiKey": "Clave de API de Tailscale",
|
||||
"tailscaleApiKeyDescription": "Se utiliza para la detección de dispositivos en el editor del host. Admite claves de Tailscale (tskey-) y Headscale (hskey-). Genere una clave en tailscale.com/admin/settings/keys o desde su instancia de Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Ver documentación",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protocolo",
|
||||
"domainLabel": "Dominio",
|
||||
"domainPlaceholder": "opcional",
|
||||
"connectToRdp": "Conéctese mediante RDP.",
|
||||
"connectToVnc": "Conéctese a través de VNC.",
|
||||
"connectToTerminal": "Conectar a Terminal",
|
||||
"connectToFiles": "Conectar a Archivos"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Establece valores que heredará cada host a menos que los sobrescriba.",
|
||||
"manage": "Administrar",
|
||||
"dialogDescription": "Esto se aplica a los hosts que dejan una configuración sin establecer. Cualquier configuración establecida en un host prevalecerá.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Escritorio remoto",
|
||||
"inherit": "Heredar",
|
||||
"on": "En",
|
||||
"off": "Apagado",
|
||||
"resizeDisplayUpdate": "Actualización de pantalla",
|
||||
"resizeReconnect": "Reconectar",
|
||||
"clearAll": "Borrar todo",
|
||||
"saved": "Se guardaron los valores predeterminados de conexión.",
|
||||
"saveFailed": "No se pudieron guardar los valores predeterminados de conexión."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Yhteystiedot",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Etätyöpöytä",
|
||||
"port": "Portti",
|
||||
"username": "Käyttäjätunnus",
|
||||
"folder": "Kansio",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Yleistä",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminaali",
|
||||
"tabRdp": "Maaseudun kehittämisohjelma",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Isännän mittarit",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Jakaminen",
|
||||
"tabAuthentication": "Todennus",
|
||||
"terminal": "Terminaali",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Poista kaikkien valinta",
|
||||
"protocols": "Protokollat",
|
||||
"secureShell": "Suojattu kuori",
|
||||
"virtualNetwork": "Virtuaaliverkko",
|
||||
"unencryptedShell": "Salaamaton kuori",
|
||||
"addressIp": "Osoite / IP-osoite",
|
||||
"friendlyName": "Ystävällinen nimi",
|
||||
"macAddress": "MAC-osoite",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Vaihe CA",
|
||||
"filterProtocolGroup": "Protokolla",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "Maaseudun kehittämisohjelma",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Ominaisuudet",
|
||||
"filterFeatureTerminal": "Terminaali",
|
||||
"filterFeatureFileManager": "Tiedostonhallinta",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Suoraan",
|
||||
"authTypeCredential": "Valtakirja",
|
||||
"authTypeNone": "Ei mitään",
|
||||
"colorDepth": "Värisyvyys",
|
||||
"resizeMethod": "Koon muuttamismenetelmä",
|
||||
"recordingPath": "Tallennuspolku",
|
||||
"recordingName": "Tallenteen nimi",
|
||||
"saveHostFirst": "Tallenna ensin isäntä.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Ota guacamole käyttöön",
|
||||
"enableGuacamoleDesc": "RDP/VNC-etätyöpöytä",
|
||||
"enableGuacamoleDocsLink": "Näytä dokumentit",
|
||||
"guacdUrl": "guacd-URL-osoite",
|
||||
"tailscaleApiKey": "Tailscale API-avain",
|
||||
"tailscaleApiKeyDescription": "Käytetään laitteiden etsimiseen isäntäeditorissa. Tukee Tailscale (tskey-) ja Headscale (hskey-) -avaimia. Luo avain osoitteessa tailscale.com/admin/settings/keys tai Headscale-instanssistasi.",
|
||||
"tailscaleApiKeyDocsLink": "Näytä dokumentit",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protokolla",
|
||||
"domainLabel": "Verkkotunnus",
|
||||
"domainPlaceholder": "valinnainen",
|
||||
"connectToRdp": "Yhdistä RDP:n kautta",
|
||||
"connectToVnc": "Yhdistä VNC:n kautta",
|
||||
"connectToTerminal": "Yhdistä terminaaliin",
|
||||
"connectToFiles": "Yhdistä tiedostoihin"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Aseta arvot, jotka jokainen isäntä perii, ellei se korvaa niitä.",
|
||||
"manage": "Hallitse",
|
||||
"dialogDescription": "Nämä koskevat isäntiä, jotka jättävät asetuksen asettamatta. Mikä tahansa isännälle asetettu arvo voittaa.",
|
||||
"tabTerminal": "Terminaali",
|
||||
"tabRemoteDesktop": "Etätyöpöytä",
|
||||
"inherit": "Periä",
|
||||
"on": "Päällä",
|
||||
"off": "Pois",
|
||||
"resizeDisplayUpdate": "Näyttöpäivitys",
|
||||
"resizeReconnect": "Yhdistä uudelleen",
|
||||
"clearAll": "Tyhjennä kaikki",
|
||||
"saved": "Yhteysoletukset tallennettu",
|
||||
"saveFailed": "Yhteysasetusten tallentaminen epäonnistui"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Détails de la connexion",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Bureau à distance",
|
||||
"port": "Port",
|
||||
"username": "Nom d'utilisateur",
|
||||
"folder": "Dossier",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Général",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Métriques de l'hôte",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Partage",
|
||||
"tabAuthentication": "Authentification",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Tout désélectionner",
|
||||
"protocols": "Protocoles",
|
||||
"secureShell": "Secure Shell",
|
||||
"virtualNetwork": "Réseau virtuel",
|
||||
"unencryptedShell": "Shell non chiffré",
|
||||
"addressIp": "Adresse / IP",
|
||||
"friendlyName": "Nom convivial",
|
||||
"macAddress": "Adresse MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Étape CA",
|
||||
"filterProtocolGroup": "Protocole",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Fonctionnalités",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Gestionnaire de fichiers",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Direct",
|
||||
"authTypeCredential": "Identifiant",
|
||||
"authTypeNone": "Aucun",
|
||||
"colorDepth": "Profondeur de couleur",
|
||||
"resizeMethod": "Méthode de redimensionnement",
|
||||
"recordingPath": "Chemin d'enregistrement",
|
||||
"recordingName": "Nom de l'enregistrement",
|
||||
"saveHostFirst": "Enregistrez d'abord l'hôte.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Activer Guacamole",
|
||||
"enableGuacamoleDesc": "Bureau à distance RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Voir la documentation",
|
||||
"guacdUrl": "URL guacd",
|
||||
"tailscaleApiKey": "Clé API Tailscale",
|
||||
"tailscaleApiKeyDescription": "Utilisé pour la détection des périphériques dans l'éditeur d'hôtes. Compatible avec les clés Tailscale (tskey-) et Headscale (hskey-). Générez une clé sur tailscale.com/admin/settings/keys ou depuis votre instance Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Voir la documentation",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protocole",
|
||||
"domainLabel": "Domaine",
|
||||
"domainPlaceholder": "facultatif",
|
||||
"connectToRdp": "Se connecter via RDP",
|
||||
"connectToVnc": "Connexion via VNC",
|
||||
"connectToTerminal": "Se connecter au terminal",
|
||||
"connectToFiles": "Se connecter aux fichiers"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Définissez des valeurs que chaque hôte hérite, sauf s'il les remplace.",
|
||||
"manage": "Gérer",
|
||||
"dialogDescription": "Ces règles s'appliquent aux hôtes qui n'ont pas défini ce paramètre. Tout paramètre défini sur l'hôte prévaut.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Bureau à distance",
|
||||
"inherit": "Hériter",
|
||||
"on": "Sur",
|
||||
"off": "Désactivé",
|
||||
"resizeDisplayUpdate": "Mise à jour de l'affichage",
|
||||
"resizeReconnect": "Reconnect",
|
||||
"clearAll": "Effacer tout",
|
||||
"saved": "Paramètres de connexion par défaut enregistrés",
|
||||
"saveFailed": "Impossible d'enregistrer les paramètres de connexion par défaut."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "פרטי חיבור",
|
||||
"ssh": "SSH",
|
||||
"telnet": "טלנט",
|
||||
"remoteDesktop": "שולחן עבודה מרוחק",
|
||||
"port": "נָמָל",
|
||||
"username": "שם משתמש",
|
||||
"folder": "תיקייה",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "כְּלָלִי",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "מָסוֹף",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "מדדי מארח",
|
||||
"tabTelnet": "טלנט",
|
||||
"tabSharing": "שיתוף",
|
||||
"tabAuthentication": "אימות",
|
||||
"terminal": "מָסוֹף",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "בטל את הבחירה של הכל",
|
||||
"protocols": "פרוטוקולים",
|
||||
"secureShell": "מעטפת מאובטחת",
|
||||
"virtualNetwork": "רשת וירטואלית",
|
||||
"unencryptedShell": "מעטפת לא מוצפנת",
|
||||
"addressIp": "כתובת / IP",
|
||||
"friendlyName": "שם ידידותי",
|
||||
"macAddress": "כתובת MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "שלב CA",
|
||||
"filterProtocolGroup": "פּרוֹטוֹקוֹל",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "טלנט",
|
||||
"filterFeaturesGroup": "תכונות",
|
||||
"filterFeatureTerminal": "מָסוֹף",
|
||||
"filterFeatureFileManager": "מנהל הקבצים",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "יָשִׁיר",
|
||||
"authTypeCredential": "תְעוּדָה",
|
||||
"authTypeNone": "אַף לֹא אֶחָד",
|
||||
"colorDepth": "עומק צבע",
|
||||
"resizeMethod": "שיטת שינוי גודל",
|
||||
"recordingPath": "נתיב ההקלטה",
|
||||
"recordingName": "שם ההקלטה",
|
||||
"saveHostFirst": "שמור את המארח קודם.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "הפעל גוואקמולי",
|
||||
"enableGuacamoleDesc": "שולחן עבודה מרוחק RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "הצג מסמכים",
|
||||
"guacdUrl": "כתובת אתר של guacd",
|
||||
"tailscaleApiKey": "מפתח ה-API של Tailscale",
|
||||
"tailscaleApiKeyDescription": "משמש לגילוי מכשירים בעורך המארח. תומך במפתחות Tailscale (tskey-) ו-Headscale (hskey-). צור מפתח בכתובת tailscale.com/admin/settings/keys או ממופע Headscale שלך.",
|
||||
"tailscaleApiKeyDocsLink": "הצג מסמכים",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "פּרוֹטוֹקוֹל",
|
||||
"domainLabel": "תְחוּם",
|
||||
"domainPlaceholder": "אופציונלי",
|
||||
"connectToRdp": "התחברות דרך RDP",
|
||||
"connectToVnc": "התחברות דרך VNC",
|
||||
"connectToTerminal": "התחברות לטרמינל",
|
||||
"connectToFiles": "התחברות לקבצים"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "הגדר ערכים שכל מארח יורש אלא אם כן הוא עוקף אותם.",
|
||||
"manage": "לְנַהֵל",
|
||||
"dialogDescription": "אלה חלים על מארחים שמשאירים הגדרה לא מוגדרת. כל דבר שמוגדר על מארח מנצח.",
|
||||
"tabTerminal": "מָסוֹף",
|
||||
"tabRemoteDesktop": "שולחן עבודה מרוחק",
|
||||
"inherit": "לָרֶשֶׁת",
|
||||
"on": "עַל",
|
||||
"off": "כבוי",
|
||||
"resizeDisplayUpdate": "עדכון תצוגה",
|
||||
"resizeReconnect": "התחבר מחדש",
|
||||
"clearAll": "נקה הכל",
|
||||
"saved": "ברירות מחדל של החיבור נשמרו",
|
||||
"saveFailed": "נכשלה שמירת ברירות המחדל של החיבור"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "कनेक्शन विवरण",
|
||||
"ssh": "SSH",
|
||||
"telnet": "टेलनेट",
|
||||
"remoteDesktop": "दूरस्थ डेस्कटॉप",
|
||||
"port": "पोर्ट",
|
||||
"username": "उपयोगकर्ता नाम",
|
||||
"folder": "फ़ोल्डर",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "सामान्य",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "टर्मिनल",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "होस्ट मेट्रिक्स",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "साझाकरण",
|
||||
"tabAuthentication": "प्रमाणीकरण",
|
||||
"terminal": "टर्मिनल",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "सभी अचयनित करें",
|
||||
"protocols": "प्रोटोकॉल",
|
||||
"secureShell": "सुरक्षित शेल",
|
||||
"virtualNetwork": "वर्चुअल नेटवर्क",
|
||||
"unencryptedShell": "अनएन्क्रिप्टेड शेल",
|
||||
"addressIp": "पता / IP",
|
||||
"friendlyName": "अनुकूल नाम",
|
||||
"macAddress": "MAC पता",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "स्टेप सीए",
|
||||
"filterProtocolGroup": "प्रोटोकॉल",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "विशेषताएं",
|
||||
"filterFeatureTerminal": "टर्मिनल",
|
||||
"filterFeatureFileManager": "फ़ाइल प्रबंधक",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "सीधा",
|
||||
"authTypeCredential": "क्रेडेंशियल",
|
||||
"authTypeNone": "कोई नहीं",
|
||||
"colorDepth": "रंग गहराई",
|
||||
"resizeMethod": "रीसाइज़ विधि",
|
||||
"recordingPath": "रिकॉर्डिंग पथ",
|
||||
"recordingName": "रिकॉर्डिंग नाम",
|
||||
"saveHostFirst": "पहले होस्ट सहेजें।",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Guacamole सक्षम करें",
|
||||
"enableGuacamoleDesc": "RDP/VNC रिमोट डेस्कटॉप",
|
||||
"enableGuacamoleDocsLink": "दस्तावेज़ देखें",
|
||||
"guacdUrl": "guacd URL",
|
||||
"tailscaleApiKey": "Tailscale API कुंजी",
|
||||
"tailscaleApiKeyDescription": "होस्ट एडिटर में डिवाइस की पहचान के लिए उपयोग किया जाता है। यह टेलस्केल (tskey-) और हेडस्केल (hskey-) कुंजियों का समर्थन करता है। आप tailscale.com/admin/settings/keys पर जाकर या अपने हेडस्केल इंस्टेंस से कुंजी जनरेट कर सकते हैं।",
|
||||
"tailscaleApiKeyDocsLink": "दस्तावेज़ देखें",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "शिष्टाचार",
|
||||
"domainLabel": "कार्यक्षेत्र",
|
||||
"domainPlaceholder": "वैकल्पिक",
|
||||
"connectToRdp": "आरडीपी के माध्यम से कनेक्ट करें",
|
||||
"connectToVnc": "VNC के माध्यम से कनेक्ट करें",
|
||||
"connectToTerminal": "टर्मिनल से कनेक्ट करें",
|
||||
"connectToFiles": "फ़ाइलों से कनेक्ट करें"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "ऐसे मान निर्धारित करें जिन्हें प्रत्येक होस्ट विरासत में प्राप्त करेगा, जब तक कि वह उन्हें ओवरराइड न कर दे।",
|
||||
"manage": "प्रबंधित करना",
|
||||
"dialogDescription": "ये नियम उन होस्ट पर लागू होते हैं जो कोई सेटिंग नहीं छोड़ते हैं। होस्ट पर जो भी सेटिंग सेट होती है, वही मान्य होती है।",
|
||||
"tabTerminal": "टर्मिनल",
|
||||
"tabRemoteDesktop": "दूरवर्ती डेस्कटॉप",
|
||||
"inherit": "इनहेरिट",
|
||||
"on": "पर",
|
||||
"off": "बंद",
|
||||
"resizeDisplayUpdate": "डिस्प्ले अपडेट",
|
||||
"resizeReconnect": "रिकनेक्ट",
|
||||
"clearAll": "सभी साफ करें",
|
||||
"saved": "कनेक्शन डिफ़ॉल्ट सहेजे गए",
|
||||
"saveFailed": "कनेक्शन डिफ़ॉल्ट सहेजने में विफल"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Kapcsolat részletei",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Távoli asztal",
|
||||
"port": "Kikötő",
|
||||
"username": "Felhasználónév",
|
||||
"folder": "Mappa",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Általános",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminál",
|
||||
"tabRdp": "Vidékfejlesztési Program",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Gazdagép-metrikák",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Megosztás",
|
||||
"tabAuthentication": "Hitelesítés",
|
||||
"terminal": "Terminál",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Összes kijelölés törlése",
|
||||
"protocols": "Protokollok",
|
||||
"secureShell": "Biztonságos shell",
|
||||
"virtualNetwork": "Virtuális hálózat",
|
||||
"unencryptedShell": "Titkosítatlan shell",
|
||||
"addressIp": "Cím / IP-cím",
|
||||
"friendlyName": "Barátságos név",
|
||||
"macAddress": "MAC-cím",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "CA lépés",
|
||||
"filterProtocolGroup": "Jegyzőkönyv",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "Vidékfejlesztési Program",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Jellemzők",
|
||||
"filterFeatureTerminal": "Terminál",
|
||||
"filterFeatureFileManager": "Fájlkezelő",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Közvetlen",
|
||||
"authTypeCredential": "Hitelesítő adat",
|
||||
"authTypeNone": "Egyik sem",
|
||||
"colorDepth": "Színmélység",
|
||||
"resizeMethod": "Átméretezési módszer",
|
||||
"recordingPath": "Felvételi útvonal",
|
||||
"recordingName": "Felvétel neve",
|
||||
"saveHostFirst": "Először mentsd el a gazdagépet.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Guacamole engedélyezése",
|
||||
"enableGuacamoleDesc": "RDP/VNC távoli asztal",
|
||||
"enableGuacamoleDocsLink": "Dokumentumok megtekintése",
|
||||
"guacdUrl": "guacd URL",
|
||||
"tailscaleApiKey": "Tailscale API-kulcs",
|
||||
"tailscaleApiKeyDescription": "Eszközfelderítésre használatos a gazdagép-szerkesztőben. Támogatja a Tailscale (tskey-) és Headscale (hskey-) kulcsokat. Kulcsot generálhat a tailscale.com/admin/settings/keys oldalon vagy a Headscale-példányából.",
|
||||
"tailscaleApiKeyDocsLink": "Dokumentumok megtekintése",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Jegyzőkönyv",
|
||||
"domainLabel": "Domain",
|
||||
"domainPlaceholder": "választható",
|
||||
"connectToRdp": "Csatlakozás RDP-n keresztül",
|
||||
"connectToVnc": "Csatlakozás VNC-n keresztül",
|
||||
"connectToTerminal": "Csatlakozás a terminálhoz",
|
||||
"connectToFiles": "Kapcsolódás fájlokhoz"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Olyan értékeket állítson be, amelyeket minden host örököl, kivéve, ha felülírja őket.",
|
||||
"manage": "Kezelés",
|
||||
"dialogDescription": "Ezek azokra a hosztokra vonatkoznak, amelyek üresen hagynak egy beállítást. Bármi, ami be van állítva egy hoszton, nyer.",
|
||||
"tabTerminal": "Terminál",
|
||||
"tabRemoteDesktop": "Távoli asztal",
|
||||
"inherit": "Örököl",
|
||||
"on": "On",
|
||||
"off": "Le",
|
||||
"resizeDisplayUpdate": "Kijelzőfrissítés",
|
||||
"resizeReconnect": "Újracsatlakozás",
|
||||
"clearAll": "Összes törlése",
|
||||
"saved": "A kapcsolat alapértelmezett beállításai mentve",
|
||||
"saveFailed": "Nem sikerült menteni a kapcsolat alapértelmezett beállításait"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Detail Koneksi",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Desktop Jarak Jauh",
|
||||
"port": "Pelabuhan",
|
||||
"username": "Nama belakang",
|
||||
"folder": "Map",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Umum",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Metrik Host",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Membagikan",
|
||||
"tabAuthentication": "Autentikasi",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Batalkan Pilihan Semua",
|
||||
"protocols": "Protokol",
|
||||
"secureShell": "Secure Shell",
|
||||
"virtualNetwork": "Jaringan Virtual",
|
||||
"unencryptedShell": "Shell yang tidak terenkripsi",
|
||||
"addressIp": "Alamat / IP",
|
||||
"friendlyName": "Nama yang Ramah",
|
||||
"macAddress": "Alamat MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Langkah CA",
|
||||
"filterProtocolGroup": "Protokol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Fitur",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Pengelola File",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Langsung",
|
||||
"authTypeCredential": "Mandat",
|
||||
"authTypeNone": "Tidak ada",
|
||||
"colorDepth": "Kedalaman Warna",
|
||||
"resizeMethod": "Metode Pengubahan Ukuran",
|
||||
"recordingPath": "Jalur Perekaman",
|
||||
"recordingName": "Nama Rekaman",
|
||||
"saveHostFirst": "Selamatkan tuan rumah terlebih dahulu.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Aktifkan Guacamole",
|
||||
"enableGuacamoleDesc": "Desktop jarak jauh RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Lihat dokumen",
|
||||
"guacdUrl": "URL guacd",
|
||||
"tailscaleApiKey": "Kunci API Tailscale",
|
||||
"tailscaleApiKeyDescription": "Digunakan untuk pendeteksian perangkat di editor host. Mendukung kunci Tailscale (tskey-) dan Headscale (hskey-). Hasilkan kunci di tailscale.com/admin/settings/keys atau dari instance Headscale Anda.",
|
||||
"tailscaleApiKeyDocsLink": "Lihat dokumen",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protokol",
|
||||
"domainLabel": "Domain",
|
||||
"domainPlaceholder": "opsional",
|
||||
"connectToRdp": "Terhubung melalui RDP",
|
||||
"connectToVnc": "Terhubung melalui VNC",
|
||||
"connectToTerminal": "Hubungkan ke Terminal",
|
||||
"connectToFiles": "Menghubungkan ke File"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Tetapkan nilai-nilai yang akan diwarisi oleh setiap host kecuali jika host tersebut menimpanya.",
|
||||
"manage": "Mengelola",
|
||||
"dialogDescription": "Ketentuan ini berlaku untuk host yang tidak mengatur apa pun. Apa pun yang diatur pada host akan berlaku.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Desktop Jarak Jauh",
|
||||
"inherit": "Mewarisi",
|
||||
"on": "Pada",
|
||||
"off": "Mati",
|
||||
"resizeDisplayUpdate": "Pembaruan Tampilan",
|
||||
"resizeReconnect": "Terhubung kembali",
|
||||
"clearAll": "Hapus Semua",
|
||||
"saved": "Pengaturan koneksi default tersimpan.",
|
||||
"saveFailed": "Gagal menyimpan pengaturan default koneksi."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Dettagli connessione",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Desktop remoto",
|
||||
"port": "Porta",
|
||||
"username": "Nome utente",
|
||||
"folder": "Cartella",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Generale",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminale",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Metriche host",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Condivisione",
|
||||
"tabAuthentication": "Autenticazione",
|
||||
"terminal": "Terminale",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Deseleziona tutto",
|
||||
"protocols": "Protocolli",
|
||||
"secureShell": "Secure Shell",
|
||||
"virtualNetwork": "Rete virtuale",
|
||||
"unencryptedShell": "Shell non crittografata",
|
||||
"addressIp": "Indirizzo / IP",
|
||||
"friendlyName": "Nome descrittivo",
|
||||
"macAddress": "Indirizzo MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Passo CA",
|
||||
"filterProtocolGroup": "Protocollo",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funzionalità",
|
||||
"filterFeatureTerminal": "Terminale",
|
||||
"filterFeatureFileManager": "Gestione file",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Diretta",
|
||||
"authTypeCredential": "Credenziale",
|
||||
"authTypeNone": "Nessuno",
|
||||
"colorDepth": "Profondità colore",
|
||||
"resizeMethod": "Metodo di ridimensionamento",
|
||||
"recordingPath": "Percorso registrazione",
|
||||
"recordingName": "Nome registrazione",
|
||||
"saveHostFirst": "Salva prima l'host",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Abilita Guacamole",
|
||||
"enableGuacamoleDesc": "Desktop remoto RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Visualizza documentazione",
|
||||
"guacdUrl": "URL guacd",
|
||||
"tailscaleApiKey": "Chiave API Tailscale",
|
||||
"tailscaleApiKeyDescription": "Utilizzata per il rilevamento dei dispositivi nell'editor host. Supporta le chiavi Tailscale (tskey-) e Headscale (hskey-). Genera una chiave su tailscale.com/admin/settings/keys o dalla tua istanza Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Visualizza documentazione",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protocollo",
|
||||
"domainLabel": "Dominio",
|
||||
"domainPlaceholder": "opzionale",
|
||||
"connectToRdp": "Connettiti tramite RDP",
|
||||
"connectToVnc": "Connessione tramite VNC",
|
||||
"connectToTerminal": "Connetti al terminale",
|
||||
"connectToFiles": "Connetti ai file"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Imposta valori che ogni host eredita a meno che non li sovrascriva.",
|
||||
"manage": "Maneggio",
|
||||
"dialogDescription": "Queste impostazioni si applicano agli host che non specificano alcuna impostazione. Qualsiasi impostazione configurata su un host ha la precedenza.",
|
||||
"tabTerminal": "terminale",
|
||||
"tabRemoteDesktop": "Desktop remoto",
|
||||
"inherit": "Ereditare",
|
||||
"on": "SU",
|
||||
"off": "Spento",
|
||||
"resizeDisplayUpdate": "Aggiornamento della visualizzazione",
|
||||
"resizeReconnect": "Ricollega",
|
||||
"clearAll": "Cancella tutto",
|
||||
"saved": "Impostazioni di connessione salvate",
|
||||
"saveFailed": "Impossibile salvare le impostazioni di connessione predefinite."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "接続の詳細",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "リモートデスクトップ",
|
||||
"port": "ポート",
|
||||
"username": "ユーザー名",
|
||||
"folder": "フォルダー",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "一般",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "ターミナル",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "ホストメトリクス",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "共有",
|
||||
"tabAuthentication": "認証",
|
||||
"terminal": "ターミナル",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "すべて選択解除",
|
||||
"protocols": "プロトコル",
|
||||
"secureShell": "セキュアシェル",
|
||||
"virtualNetwork": "仮想ネットワーク",
|
||||
"unencryptedShell": "暗号化されていないシェル",
|
||||
"addressIp": "アドレス / IP",
|
||||
"friendlyName": "表示名",
|
||||
"macAddress": "MAC アドレス",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "ステップCA",
|
||||
"filterProtocolGroup": "プロトコル",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "機能",
|
||||
"filterFeatureTerminal": "ターミナル",
|
||||
"filterFeatureFileManager": "ファイルマネージャー",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "直接",
|
||||
"authTypeCredential": "認証情報",
|
||||
"authTypeNone": "なし",
|
||||
"colorDepth": "色深度",
|
||||
"resizeMethod": "リサイズ方法",
|
||||
"recordingPath": "録画パス",
|
||||
"recordingName": "録画名",
|
||||
"saveHostFirst": "最初にホストを保存してください。",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Guacamoleを有効にする",
|
||||
"enableGuacamoleDesc": "RDP/VNCリモートデスクトップ",
|
||||
"enableGuacamoleDocsLink": "ドキュメントを見る",
|
||||
"guacdUrl": "guacd URL",
|
||||
"tailscaleApiKey": "Tailscale APIキー",
|
||||
"tailscaleApiKeyDescription": "ホストエディタでのデバイス検出に使用されます。Tailscale (tskey-) および Headscale (hskey-) キーをサポートしています。キーは tailscale.com/admin/settings/keys または Headscale インスタンスから生成できます。",
|
||||
"tailscaleApiKeyDocsLink": "ドキュメントを見る",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "プロトコル",
|
||||
"domainLabel": "ドメイン",
|
||||
"domainPlaceholder": "オプション",
|
||||
"connectToRdp": "RDP経由で接続する",
|
||||
"connectToVnc": "VN経由で接続",
|
||||
"connectToTerminal": "ターミナルに接続",
|
||||
"connectToFiles": "ファイルに接続"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "すべてのホストが継承する値を設定します。ただし、ホストが上書きする場合は除きます。",
|
||||
"manage": "管理",
|
||||
"dialogDescription": "これは、設定が未設定のホストに適用されます。ホストで設定されている内容が優先されます。",
|
||||
"tabTerminal": "ターミナル",
|
||||
"tabRemoteDesktop": "リモートデスクトップ",
|
||||
"inherit": "継承する",
|
||||
"on": "の上",
|
||||
"off": "オフ",
|
||||
"resizeDisplayUpdate": "ディスプレイの更新",
|
||||
"resizeReconnect": "再接続",
|
||||
"clearAll": "すべてクリア",
|
||||
"saved": "接続のデフォルト設定が保存されました",
|
||||
"saveFailed": "接続設定の保存に失敗しました"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "연결 세부 정보",
|
||||
"ssh": "SSH",
|
||||
"telnet": "텔넷",
|
||||
"remoteDesktop": "원격 데스크톱",
|
||||
"port": "포트",
|
||||
"username": "사용자 이름",
|
||||
"folder": "폴더",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "일반",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "터미널",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "호스트 메트릭",
|
||||
"tabTelnet": "텔넷",
|
||||
"tabSharing": "공유",
|
||||
"tabAuthentication": "인증",
|
||||
"terminal": "터미널",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "전체 선택 해제",
|
||||
"protocols": "프로토콜",
|
||||
"secureShell": "보안 셸",
|
||||
"virtualNetwork": "가상 네트워크",
|
||||
"unencryptedShell": "암호화되지 않은 셸",
|
||||
"addressIp": "주소 / IP",
|
||||
"friendlyName": "친숙한 이름",
|
||||
"macAddress": "MAC 주소",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "스텝 CA",
|
||||
"filterProtocolGroup": "프로토콜",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "텔넷",
|
||||
"filterFeaturesGroup": "기능",
|
||||
"filterFeatureTerminal": "터미널",
|
||||
"filterFeatureFileManager": "파일 관리자",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "직접",
|
||||
"authTypeCredential": "자격 증명",
|
||||
"authTypeNone": "없음",
|
||||
"colorDepth": "색 농도",
|
||||
"resizeMethod": "크기 조정 방법",
|
||||
"recordingPath": "녹화 경로",
|
||||
"recordingName": "녹화 이름",
|
||||
"saveHostFirst": "호스트를 먼저 저장하세요.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Guacamole 활성화",
|
||||
"enableGuacamoleDesc": "RDP/VNC 원격 데스크톱",
|
||||
"enableGuacamoleDocsLink": "문서 보기",
|
||||
"guacdUrl": "guacd URL",
|
||||
"tailscaleApiKey": "Tailscale API 키",
|
||||
"tailscaleApiKeyDescription": "호스트 편집기에서 장치 검색에 사용됩니다. Tailscale(tskey-) 및 Headscale(hskey-) 키를 지원합니다. 키는 tailscale.com/admin/settings/keys 또는 Headscale 인스턴스에서 생성할 수 있습니다.",
|
||||
"tailscaleApiKeyDocsLink": "문서 보기",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "규약",
|
||||
"domainLabel": "도메인",
|
||||
"domainPlaceholder": "선택 과목",
|
||||
"connectToRdp": "RDP를 통해 연결",
|
||||
"connectToVnc": "VNC를 통해 연결",
|
||||
"connectToTerminal": "터미널에 연결",
|
||||
"connectToFiles": "파일에 연결"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "호스트가 재정의하지 않는 한 모든 호스트가 상속받는 값을 설정합니다.",
|
||||
"manage": "관리하다",
|
||||
"dialogDescription": "이 설정은 호스트에서 설정을 지정하지 않은 경우에 적용됩니다. 호스트에 설정된 값이 우선 적용됩니다.",
|
||||
"tabTerminal": "단말기",
|
||||
"tabRemoteDesktop": "원격 데스크톱",
|
||||
"inherit": "상속하다",
|
||||
"on": "~에",
|
||||
"off": "끄다",
|
||||
"resizeDisplayUpdate": "디스플레이 업데이트",
|
||||
"resizeReconnect": "다시 연결",
|
||||
"clearAll": "모두 지우기",
|
||||
"saved": "연결 기본 설정이 저장되었습니다.",
|
||||
"saveFailed": "연결 기본 설정을 저장하는 데 실패했습니다."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Verbindingsgegevens",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Extern bureaublad",
|
||||
"port": "Haven",
|
||||
"username": "Gebruikersnaam",
|
||||
"folder": "Map",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Algemeen",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Hoststatistieken",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Delen",
|
||||
"tabAuthentication": "Authenticatie",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Alles deselecteren",
|
||||
"protocols": "Protocollen",
|
||||
"secureShell": "Secure Shell",
|
||||
"virtualNetwork": "Virtueel netwerk",
|
||||
"unencryptedShell": "Niet-versleutelde shell",
|
||||
"addressIp": "Adres / IP-adres",
|
||||
"friendlyName": "Vriendelijke naam",
|
||||
"macAddress": "MAC-adres",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Stap CA",
|
||||
"filterProtocolGroup": "Protocol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Functies",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Bestandsbeheerder",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Direct",
|
||||
"authTypeCredential": "Referentie",
|
||||
"authTypeNone": "Geen",
|
||||
"colorDepth": "Kleurdiepte",
|
||||
"resizeMethod": "Formaatwijzigingsmethode",
|
||||
"recordingPath": "Opnamepad",
|
||||
"recordingName": "Opnamenaam",
|
||||
"saveHostFirst": "Sla eerst de host op.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Schakel Guacamole in",
|
||||
"enableGuacamoleDesc": "RDP/VNC-extern bureaublad",
|
||||
"enableGuacamoleDocsLink": "Documenten bekijken",
|
||||
"guacdUrl": "guacd URL",
|
||||
"tailscaleApiKey": "Tailscale API-sleutel",
|
||||
"tailscaleApiKeyDescription": "Wordt gebruikt voor apparaatdetectie in de hosteditor. Ondersteunt Tailscale (tskey-) en Headscale (hskey-) sleutels. Genereer een sleutel op tailscale.com/admin/settings/keys of vanuit uw Headscale-instantie.",
|
||||
"tailscaleApiKeyDocsLink": "Documenten bekijken",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protocol",
|
||||
"domainLabel": "Domein",
|
||||
"domainPlaceholder": "optioneel",
|
||||
"connectToRdp": "Verbinden via RDP",
|
||||
"connectToVnc": "Verbinden via VNC",
|
||||
"connectToTerminal": "Verbinden met de terminal",
|
||||
"connectToFiles": "Verbinding maken met bestanden"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Stel waarden in die elke host overneemt, tenzij deze ze overschrijft.",
|
||||
"manage": "Beheren",
|
||||
"dialogDescription": "Dit geldt voor hosts die een instelling niet hebben ingesteld. Alles wat op een host is ingesteld, heeft voorrang.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Extern bureaublad",
|
||||
"inherit": "Erven",
|
||||
"on": "Op",
|
||||
"off": "Uit",
|
||||
"resizeDisplayUpdate": "Weergave bijwerken",
|
||||
"resizeReconnect": "Opnieuw verbinding maken",
|
||||
"clearAll": "Alles wissen",
|
||||
"saved": "Verbindingsstandaarden opgeslagen",
|
||||
"saveFailed": "Het opslaan van de verbindingsinstellingen is mislukt."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Tilkoblingsdetaljer",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Eksternt skrivebord",
|
||||
"port": "Havn",
|
||||
"username": "Brukernavn",
|
||||
"folder": "Mappe",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "General",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Vertsmålinger",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Deling",
|
||||
"tabAuthentication": "Autentisering",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Fjern merkingen av alle",
|
||||
"protocols": "Protokoller",
|
||||
"secureShell": "Sikkert skall",
|
||||
"virtualNetwork": "Virtuelt nettverk",
|
||||
"unencryptedShell": "Ukryptert skall",
|
||||
"addressIp": "Adresse / IP",
|
||||
"friendlyName": "Vennlig navn",
|
||||
"macAddress": "MAC-adresse",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Trinn CA",
|
||||
"filterProtocolGroup": "Protokoll",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funksjoner",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Filbehandler",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Direkte",
|
||||
"authTypeCredential": "Legitimasjon",
|
||||
"authTypeNone": "Ingen",
|
||||
"colorDepth": "Fargedybde",
|
||||
"resizeMethod": "Endre størrelse-metoden",
|
||||
"recordingPath": "Opptakssti",
|
||||
"recordingName": "Opptaksnavn",
|
||||
"saveHostFirst": "Lagre verten først.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Aktiver guacamole",
|
||||
"enableGuacamoleDesc": "RDP/VNC eksternt skrivebord",
|
||||
"enableGuacamoleDocsLink": "Vis dokumenter",
|
||||
"guacdUrl": "guacd-URL",
|
||||
"tailscaleApiKey": "Tailscale API-nøkkel",
|
||||
"tailscaleApiKeyDescription": "Brukes for enhetsoppdagelse i vertredigeringsprogrammet. Støtter Tailscale- (tskey-) og Headscale- (hskey-) nøkler. Generer en nøkkel på tailscale.com/admin/settings/keys eller fra Headscale-instansen din.",
|
||||
"tailscaleApiKeyDocsLink": "Vis dokumenter",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protokoll",
|
||||
"domainLabel": "Domene",
|
||||
"domainPlaceholder": "valgfri",
|
||||
"connectToRdp": "Koble til via RDP",
|
||||
"connectToVnc": "Koble til via VNC",
|
||||
"connectToTerminal": "Koble til terminalen",
|
||||
"connectToFiles": "Koble til filer"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Angi verdier som hver vert arver med mindre den overstyrer dem.",
|
||||
"manage": "Administrer",
|
||||
"dialogDescription": "Disse gjelder for verter som ikke angir en innstilling. Alt som er angitt på en vert vinner.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Eksternt skrivebord",
|
||||
"inherit": "Arve",
|
||||
"on": "På",
|
||||
"off": "Av",
|
||||
"resizeDisplayUpdate": "Oppdatering av skjerm",
|
||||
"resizeReconnect": "Koble til igjen",
|
||||
"clearAll": "Fjern alt",
|
||||
"saved": "Standardinnstillinger for tilkobling lagret",
|
||||
"saveFailed": "Kunne ikke lagre standardinnstillinger for tilkobling"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Szczegóły połączenia",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Pulpit zdalny",
|
||||
"port": "Port",
|
||||
"username": "Nazwa użytkownika",
|
||||
"folder": "Folder",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Ogólne",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Metryki hosta",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Udostępnianie",
|
||||
"tabAuthentication": "Uwierzytelnianie",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Odznacz wszystko",
|
||||
"protocols": "Protokoły",
|
||||
"secureShell": "Bezpieczna powłoka",
|
||||
"virtualNetwork": "Sieć wirtualna",
|
||||
"unencryptedShell": "Niezaszyfrowana powłoka",
|
||||
"addressIp": "Adres / IP",
|
||||
"friendlyName": "Przyjazna nazwa",
|
||||
"macAddress": "Adres MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Krok CA",
|
||||
"filterProtocolGroup": "Protokół",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funkcje",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Menedżer plików",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Bezpośredni",
|
||||
"authTypeCredential": "Poświadczenie",
|
||||
"authTypeNone": "Brak",
|
||||
"colorDepth": "Głębia koloru",
|
||||
"resizeMethod": "Metoda zmiany rozmiaru",
|
||||
"recordingPath": "Ścieżka nagrywania",
|
||||
"recordingName": "Nazwa nagrania",
|
||||
"saveHostFirst": "Najpierw zapisz hosta.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Włącz Guacamole",
|
||||
"enableGuacamoleDesc": "Zdalny pulpit RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Zobacz dokumentację",
|
||||
"guacdUrl": "Adres URL guacd",
|
||||
"tailscaleApiKey": "Klucz API Tailscale",
|
||||
"tailscaleApiKeyDescription": "Służy do wykrywania urządzeń w edytorze hostów. Obsługuje klucze Tailscale (tskey-) i Headscale (hskey-). Wygeneruj klucz na tailscale.com/admin/settings/keys lub w swojej instancji Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Zobacz dokumentację",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protokół",
|
||||
"domainLabel": "Domena",
|
||||
"domainPlaceholder": "fakultatywny",
|
||||
"connectToRdp": "Połącz przez RDP",
|
||||
"connectToVnc": "Połącz przez VNC",
|
||||
"connectToTerminal": "Połącz z terminalem",
|
||||
"connectToFiles": "Połącz z plikami"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Ustaw wartości, które dziedziczy każdy host, chyba że je nadpisze.",
|
||||
"manage": "Zarządzaj",
|
||||
"dialogDescription": "Dotyczą hostów, które nie mają ustawionej danej opcji. Ustawienie na hoście ma pierwszeństwo.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Pulpit zdalny",
|
||||
"inherit": "Dziedzicz",
|
||||
"on": "Wł.",
|
||||
"off": "Wył.",
|
||||
"resizeDisplayUpdate": "Aktualizacja ekranu",
|
||||
"resizeReconnect": "Połącz ponownie",
|
||||
"clearAll": "Wyczyść wszystko",
|
||||
"saved": "Zapisano domyślne ustawienia połączenia",
|
||||
"saveFailed": "Nie udało się zapisać domyślnych ustawień połączenia"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Detalhes da conexão",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Área de Trabalho Remota",
|
||||
"port": "Porta",
|
||||
"username": "Nome de usuário",
|
||||
"folder": "Pasta",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Em geral",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Métricas do host",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Compartilhamento",
|
||||
"tabAuthentication": "Autenticação",
|
||||
"terminal": "terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Desmarcar tudo",
|
||||
"protocols": "Protocolos",
|
||||
"secureShell": "Secure Shell",
|
||||
"virtualNetwork": "Rede Virtual",
|
||||
"unencryptedShell": "Shell não criptografado",
|
||||
"addressIp": "Endereço IP /",
|
||||
"friendlyName": "Nome amigável",
|
||||
"macAddress": "Endereço MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Etapa CA",
|
||||
"filterProtocolGroup": "Protocolo",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Características",
|
||||
"filterFeatureTerminal": "terminal",
|
||||
"filterFeatureFileManager": "Gerenciador de arquivos",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Direto",
|
||||
"authTypeCredential": "Credencial",
|
||||
"authTypeNone": "Nenhum",
|
||||
"colorDepth": "Profundidade de cor",
|
||||
"resizeMethod": "Método de redimensionamento",
|
||||
"recordingPath": "Caminho de gravação",
|
||||
"recordingName": "Nome da gravação",
|
||||
"saveHostFirst": "Primeiro, salve o host.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Ativar Guacamole",
|
||||
"enableGuacamoleDesc": "Área de trabalho remota RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Ver documentos",
|
||||
"guacdUrl": "URL do guacd",
|
||||
"tailscaleApiKey": "Chave da API Tailscale",
|
||||
"tailscaleApiKeyDescription": "Utilizado para descoberta de dispositivos no editor de hosts. Compatível com chaves Tailscale (tskey-) e Headscale (hskey-). Gere uma chave em tailscale.com/admin/settings/keys ou a partir da sua instância Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Ver documentos",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protocolo",
|
||||
"domainLabel": "Domínio",
|
||||
"domainPlaceholder": "opcional",
|
||||
"connectToRdp": "Conectar via RDP",
|
||||
"connectToVnc": "Conecte-se via VNC",
|
||||
"connectToTerminal": "Conectar ao terminal",
|
||||
"connectToFiles": "Conectar aos arquivos"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Defina valores que todos os hosts herdarão, a menos que os substituam.",
|
||||
"manage": "Gerenciar",
|
||||
"dialogDescription": "Essas configurações se aplicam a hosts que deixam alguma configuração indefinida. Qualquer configuração definida no host prevalece.",
|
||||
"tabTerminal": "terminal",
|
||||
"tabRemoteDesktop": "Área de Trabalho Remota",
|
||||
"inherit": "Herdar",
|
||||
"on": "Sobre",
|
||||
"off": "Desligado",
|
||||
"resizeDisplayUpdate": "Atualização de exibição",
|
||||
"resizeReconnect": "Reconectar",
|
||||
"clearAll": "Limpar tudo",
|
||||
"saved": "Configurações de conexão salvas",
|
||||
"saveFailed": "Falha ao salvar as configurações padrão de conexão."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Detalhes da ligação",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Ambiente de trabalho remoto",
|
||||
"port": "Porta",
|
||||
"username": "Nome de utilizador",
|
||||
"folder": "Pasta",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Geral",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Métricas do Host",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Partilha",
|
||||
"tabAuthentication": "Autenticação",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Desmarcar Todos",
|
||||
"protocols": "Protocolos",
|
||||
"secureShell": "Shell Seguro",
|
||||
"virtualNetwork": "Rede Virtual",
|
||||
"unencryptedShell": "Shell não encriptado",
|
||||
"addressIp": "Endereço / IP",
|
||||
"friendlyName": "Nome Amigável",
|
||||
"macAddress": "Endereço MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Etapa CA",
|
||||
"filterProtocolGroup": "Protocolo",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Funcionalidades",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Gestor de Ficheiros",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Direta",
|
||||
"authTypeCredential": "Credencial",
|
||||
"authTypeNone": "Nenhum",
|
||||
"colorDepth": "Profundidade de cor",
|
||||
"resizeMethod": "Método de redimensionamento",
|
||||
"recordingPath": "Caminho da gravação",
|
||||
"recordingName": "Nome da gravação",
|
||||
"saveHostFirst": "Guarde o host primeiro.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Ativar Guacamole",
|
||||
"enableGuacamoleDesc": "Ambiente de trabalho remoto RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Ver documentação",
|
||||
"guacdUrl": "URL do guacd",
|
||||
"tailscaleApiKey": "Chave de API do Tailscale",
|
||||
"tailscaleApiKeyDescription": "Utilizado para a descoberta de dispositivos no editor de hosts. Compatível com as chaves Tailscale (tskey-) e Headscale (hskey-). Gere uma chave em tailscale.com/admin/settings/keys ou a partir da sua instância Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Ver documentação",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protocolo",
|
||||
"domainLabel": "Domínio",
|
||||
"domainPlaceholder": "opcional",
|
||||
"connectToRdp": "Ligar via RDP",
|
||||
"connectToVnc": "Ligue-se via VNC",
|
||||
"connectToTerminal": "Ligar ao Terminal",
|
||||
"connectToFiles": "Ligar aos Ficheiros"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Defina valores que todos os hosts herdarão, a menos que os substituam.",
|
||||
"manage": "Gerir",
|
||||
"dialogDescription": "Estas definições aplicam-se a hosts que deixam alguma configuração indefinida. Qualquer configuração definida no host prevalece.",
|
||||
"tabTerminal": "terminal",
|
||||
"tabRemoteDesktop": "Área de Trabalho Remota",
|
||||
"inherit": "Herdar",
|
||||
"on": "Em",
|
||||
"off": "Desligado",
|
||||
"resizeDisplayUpdate": "Atualização de visualização",
|
||||
"resizeReconnect": "Reconectar",
|
||||
"clearAll": "Limpar tudo",
|
||||
"saved": "Configurações de ligação guardadas",
|
||||
"saveFailed": "Falha ao guardar as definições padrão de ligação."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Detalii conexiune",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Desktop la distanță",
|
||||
"port": "Port",
|
||||
"username": "Nume de utilizator",
|
||||
"folder": "Pliant",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "General",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Metrici ale gazdei",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Partajare",
|
||||
"tabAuthentication": "Autentificare",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Deselectați tot",
|
||||
"protocols": "Protocoale",
|
||||
"secureShell": "Shell securizat",
|
||||
"virtualNetwork": "Rețea virtuală",
|
||||
"unencryptedShell": "Shell necriptat",
|
||||
"addressIp": "Adresă / IP",
|
||||
"friendlyName": "Nume prietenos",
|
||||
"macAddress": "Adresă MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Pasul CA",
|
||||
"filterProtocolGroup": "Protocol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Caracteristici",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Manager de fișiere",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Direct",
|
||||
"authTypeCredential": "Acreditare",
|
||||
"authTypeNone": "Nici unul",
|
||||
"colorDepth": "Adâncimea culorii",
|
||||
"resizeMethod": "Metodă de redimensionare",
|
||||
"recordingPath": "Calea de înregistrare",
|
||||
"recordingName": "Nume înregistrare",
|
||||
"saveHostFirst": "Salvează mai întâi gazda.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Activează Guacamole",
|
||||
"enableGuacamoleDesc": "Desktop la distanță RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Vizualizați documentele",
|
||||
"guacdUrl": "URL-ul guacd",
|
||||
"tailscaleApiKey": "Cheie API Tailscale",
|
||||
"tailscaleApiKeyDescription": "Folosit pentru descoperirea dispozitivelor în editorul gazdă. Acceptă cheile Tailscale (tskey-) și Headscale (hskey-). Generați o cheie la tailscale.com/admin/settings/keys sau din instanța Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Vizualizați documentele",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protocol",
|
||||
"domainLabel": "Domeniu",
|
||||
"domainPlaceholder": "opțional",
|
||||
"connectToRdp": "Conectați-vă prin RDP",
|
||||
"connectToVnc": "Conectați-vă prin VNC",
|
||||
"connectToTerminal": "Conectați-vă la terminal",
|
||||
"connectToFiles": "Conectare la fișiere"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Setați valorile pe care fiecare gazdă le moștenește, cu excepția cazului în care le suprascrie.",
|
||||
"manage": "Gestionează",
|
||||
"dialogDescription": "Acestea se aplică gazdelor care lasă o setare neconfigurată. Orice setare este setată pe o gazdă este câștigătoare.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Desktop la distanță",
|
||||
"inherit": "Moşteni",
|
||||
"on": "Pe",
|
||||
"off": "Dezactivat",
|
||||
"resizeDisplayUpdate": "Actualizare afișare",
|
||||
"resizeReconnect": "Reconectați-vă",
|
||||
"clearAll": "Ștergeți tot",
|
||||
"saved": "Setările implicite ale conexiunii au fost salvate",
|
||||
"saveFailed": "Nu s-au putut salva setările implicite ale conexiunii"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Детали подключения",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Удаленный рабочий стол",
|
||||
"port": "Порт",
|
||||
"username": "Имя пользователя",
|
||||
"folder": "Папка",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Общие",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Терминал",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Метрики хоста",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Общий доступ",
|
||||
"tabAuthentication": "Аутентификация",
|
||||
"terminal": "Терминал",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Снять выделение со всех",
|
||||
"protocols": "Протоколы",
|
||||
"secureShell": "SSH",
|
||||
"virtualNetwork": "Виртуальная сеть",
|
||||
"unencryptedShell": "Незашифрованная оболочка",
|
||||
"addressIp": "Адрес / IP",
|
||||
"friendlyName": "Понятное имя",
|
||||
"macAddress": "MAC-адрес",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Шаг CA",
|
||||
"filterProtocolGroup": "Протокол",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Функции",
|
||||
"filterFeatureTerminal": "Терминал",
|
||||
"filterFeatureFileManager": "Файловый менеджер",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Прямой",
|
||||
"authTypeCredential": "Учетные данные",
|
||||
"authTypeNone": "Никто",
|
||||
"colorDepth": "Глубина цвета",
|
||||
"resizeMethod": "Метод изменения размера",
|
||||
"recordingPath": "Путь записи",
|
||||
"recordingName": "Имя записи",
|
||||
"saveHostFirst": "Сначала сохраните хост.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Включить Guacamole",
|
||||
"enableGuacamoleDesc": "Удаленный рабочий стол RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Документация",
|
||||
"guacdUrl": "URL guacd",
|
||||
"tailscaleApiKey": "API-ключ Tailscale",
|
||||
"tailscaleApiKeyDescription": "Используется для обнаружения устройств в редакторе хоста. Поддерживает ключи Tailscale (tskey-) и Headscale (hskey-). Сгенерируйте ключ на сайте tailscale.com/admin/settings/keys или в вашем экземпляре Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Документация",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Протокол",
|
||||
"domainLabel": "Домен",
|
||||
"domainPlaceholder": "необязательный",
|
||||
"connectToRdp": "Подключение через RDP",
|
||||
"connectToVnc": "Подключение через VNC",
|
||||
"connectToTerminal": "Подключиться к терминалу",
|
||||
"connectToFiles": "Подключиться к файлам"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Установите значения, которые будут наследоваться каждым хостом, если он их не переопределит.",
|
||||
"manage": "Управлять",
|
||||
"dialogDescription": "Эти правила применяются к хостам, которые не задают какой-либо параметр. Любой параметр, заданный на хосте, имеет приоритет.",
|
||||
"tabTerminal": "Терминал",
|
||||
"tabRemoteDesktop": "Удаленный рабочий стол",
|
||||
"inherit": "Наследовать",
|
||||
"on": "На",
|
||||
"off": "Выключенный",
|
||||
"resizeDisplayUpdate": "Обновление дисплея",
|
||||
"resizeReconnect": "Переподключитесь",
|
||||
"clearAll": "Очистить все",
|
||||
"saved": "Сохранены параметры подключения по умолчанию.",
|
||||
"saveFailed": "Не удалось сохранить параметры подключения по умолчанию."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Детаљи везе",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Телнет",
|
||||
"remoteDesktop": "Удаљена радна површина",
|
||||
"port": "Лука",
|
||||
"username": "Корисничко име",
|
||||
"folder": "Фасцикла",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Опште",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Терминал",
|
||||
"tabRdp": "РДП",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Метрике хоста",
|
||||
"tabTelnet": "Телнет",
|
||||
"tabSharing": "Дељење",
|
||||
"tabAuthentication": "Аутентификација",
|
||||
"terminal": "Терминал",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Поништи избор свих",
|
||||
"protocols": "Протоколи",
|
||||
"secureShell": "Безбедна шкољка",
|
||||
"virtualNetwork": "Виртуелна мрежа",
|
||||
"unencryptedShell": "Нешифрована љуска",
|
||||
"addressIp": "Адреса / ИП",
|
||||
"friendlyName": "Пријатељско име",
|
||||
"macAddress": "MAC адреса",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Корак CA",
|
||||
"filterProtocolGroup": "Протокол",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "РДП",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Телнет",
|
||||
"filterFeaturesGroup": "Карактеристике",
|
||||
"filterFeatureTerminal": "Терминал",
|
||||
"filterFeatureFileManager": "Менаџер датотека",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Директно",
|
||||
"authTypeCredential": "Акредитив",
|
||||
"authTypeNone": "Ниједан",
|
||||
"colorDepth": "Дубина боје",
|
||||
"resizeMethod": "Метод промене величине",
|
||||
"recordingPath": "Путања снимања",
|
||||
"recordingName": "Назив снимка",
|
||||
"saveHostFirst": "Прво сачувајте домаћина.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Омогући гуакамоле",
|
||||
"enableGuacamoleDesc": "RDP/VNC удаљена радна површина",
|
||||
"enableGuacamoleDocsLink": "Прикажи документе",
|
||||
"guacdUrl": "URL адреса guacd-а",
|
||||
"tailscaleApiKey": "API кључ Tailscale-а",
|
||||
"tailscaleApiKeyDescription": "Користи се за откривање уређаја у уређивачу хоста. Подржава кључеве Tailscale (tkey-) и Headscale (hskey-). Генеришите кључ на tailscale.com/admin/settings/keys или из ваше Headscale инстанце.",
|
||||
"tailscaleApiKeyDocsLink": "Прикажи документе",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Протокол",
|
||||
"domainLabel": "Домен",
|
||||
"domainPlaceholder": "опционо",
|
||||
"connectToRdp": "Повежите се путем RDP-а",
|
||||
"connectToVnc": "Повежите се преко VNC-а",
|
||||
"connectToTerminal": "Повежи се са терминалом",
|
||||
"connectToFiles": "Повежи се са датотекама"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Поставите вредности које сваки хост наслеђује, осим ако их не препише.",
|
||||
"manage": "Управљај",
|
||||
"dialogDescription": "Ово се односи на хостове који не подешавају подешавање. Све што је подешено на хосту побеђује.",
|
||||
"tabTerminal": "Терминал",
|
||||
"tabRemoteDesktop": "Удаљена радна површина",
|
||||
"inherit": "Наследи",
|
||||
"on": "Укључено",
|
||||
"off": "Искључено",
|
||||
"resizeDisplayUpdate": "Ажурирање екрана",
|
||||
"resizeReconnect": "Поново се повежите",
|
||||
"clearAll": "Обриши све",
|
||||
"saved": "Подразумевана подешавања везе су сачувана",
|
||||
"saveFailed": "Чување подразумеваних подешавања везе није успело"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Anslutningsdetaljer",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Fjärrskrivbord",
|
||||
"port": "Hamn",
|
||||
"username": "Användarnamn",
|
||||
"folder": "Mapp",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Allmän",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "Landsbygdsutveckling",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Värdstatistik",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Delning",
|
||||
"tabAuthentication": "Autentisering",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Avmarkera alla",
|
||||
"protocols": "Protokoll",
|
||||
"secureShell": "Säkert skal",
|
||||
"virtualNetwork": "Virtuellt nätverk",
|
||||
"unencryptedShell": "Okrypterat skal",
|
||||
"addressIp": "Adress / IP-adress",
|
||||
"friendlyName": "Vänligt namn",
|
||||
"macAddress": "MAC-adress",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Steg CA",
|
||||
"filterProtocolGroup": "Protokoll",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "Landsbygdsutveckling",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Drag",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Filhanteraren",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Direkt",
|
||||
"authTypeCredential": "Referenser",
|
||||
"authTypeNone": "Ingen",
|
||||
"colorDepth": "Färgdjup",
|
||||
"resizeMethod": "Ändra storleksmetod",
|
||||
"recordingPath": "Inspelningsväg",
|
||||
"recordingName": "Inspelningsnamn",
|
||||
"saveHostFirst": "Spara värden först.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Aktivera guacamole",
|
||||
"enableGuacamoleDesc": "RDP/VNC-fjärrskrivbord",
|
||||
"enableGuacamoleDocsLink": "Visa dokument",
|
||||
"guacdUrl": "guacd-URL",
|
||||
"tailscaleApiKey": "Tailscale API-nyckel",
|
||||
"tailscaleApiKeyDescription": "Används för enhetsidentifiering i värdredigeraren. Stöder Tailscale- (tskey-) och Headscale- (hskey-) nycklar. Generera en nyckel på tailscale.com/admin/settings/keys eller från din Headscale-instans.",
|
||||
"tailscaleApiKeyDocsLink": "Visa dokument",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protokoll",
|
||||
"domainLabel": "Domän",
|
||||
"domainPlaceholder": "frivillig",
|
||||
"connectToRdp": "Anslut via RDP",
|
||||
"connectToVnc": "Anslut via VNC",
|
||||
"connectToTerminal": "Anslut till terminalen",
|
||||
"connectToFiles": "Anslut till filer"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Ange värden som varje värd ärver om den inte åsidosätter dem.",
|
||||
"manage": "Hantera",
|
||||
"dialogDescription": "Dessa gäller för värdar som lämnar en inställning oaktiverad. Allt som ställs in på en värd vinner.",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRemoteDesktop": "Fjärrskrivbord",
|
||||
"inherit": "Ärva",
|
||||
"on": "På",
|
||||
"off": "Av",
|
||||
"resizeDisplayUpdate": "Uppdatering av displayen",
|
||||
"resizeReconnect": "Återanslut",
|
||||
"clearAll": "Rensa alla",
|
||||
"saved": "Standardinställningar för anslutning sparade",
|
||||
"saveFailed": "Kunde inte spara standardinställningarna för anslutningen"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "รายละเอียดการเชื่อมต่อ",
|
||||
"ssh": "เอสเอช",
|
||||
"telnet": "เทลเน็ต",
|
||||
"remoteDesktop": "รีโมทเดสก์ท็อป",
|
||||
"port": "ท่าเรือ",
|
||||
"username": "ชื่อผู้ใช้",
|
||||
"folder": "โฟลเดอร์",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "ทั่วไป",
|
||||
"tabSsh": "เอสเอช",
|
||||
"tabTerminal": "เทอร์มินัล",
|
||||
"tabRdp": "อาร์ดีพี",
|
||||
"tabVnc": "วีเอ็นซี",
|
||||
"tabStats": "เมตริกโฮสต์",
|
||||
"tabTelnet": "เทลเน็ต",
|
||||
"tabSharing": "การแบ่งปัน",
|
||||
"tabAuthentication": "การตรวจสอบสิทธิ์",
|
||||
"terminal": "เทอร์มินัล",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "ยกเลิกการเลือกทั้งหมด",
|
||||
"protocols": "โปรโตคอล",
|
||||
"secureShell": "เปลือกหุ้มที่ปลอดภัย",
|
||||
"virtualNetwork": "เครือข่ายเสมือน",
|
||||
"unencryptedShell": "เชลล์ที่ไม่ได้เข้ารหัส",
|
||||
"addressIp": "ที่อยู่ / IP",
|
||||
"friendlyName": "ชื่อที่เป็นมิตร",
|
||||
"macAddress": "ที่อยู่ MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "ขั้นตอน CA",
|
||||
"filterProtocolGroup": "โปรโตคอล",
|
||||
"filterProtocolSsh": "เอสเอช",
|
||||
"filterProtocolRdp": "อาร์ดีพี",
|
||||
"filterProtocolVnc": "วีเอ็นซี",
|
||||
"filterProtocolTelnet": "เทลเน็ต",
|
||||
"filterFeaturesGroup": "คุณสมบัติ",
|
||||
"filterFeatureTerminal": "เทอร์มินัล",
|
||||
"filterFeatureFileManager": "ตัวจัดการไฟล์",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "โดยตรง",
|
||||
"authTypeCredential": "ใบรับรอง",
|
||||
"authTypeNone": "ไม่มี",
|
||||
"colorDepth": "ความลึกของสี",
|
||||
"resizeMethod": "วิธีการปรับขนาด",
|
||||
"recordingPath": "เส้นทางการบันทึก",
|
||||
"recordingName": "ชื่อการบันทึก",
|
||||
"saveHostFirst": "บันทึกข้อมูลโฮสต์ก่อน",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "เปิดใช้งาน Guacamole",
|
||||
"enableGuacamoleDesc": "การเข้าถึงเดสก์ท็อประยะไกลผ่าน RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "ดูเอกสาร",
|
||||
"guacdUrl": "URL guacd",
|
||||
"tailscaleApiKey": "คีย์ API ของ Tailscale",
|
||||
"tailscaleApiKeyDescription": "ใช้สำหรับการค้นหาอุปกรณ์ในตัวแก้ไขโฮสต์ รองรับคีย์ Tailscale (tskey-) และ Headscale (hskey-) สร้างคีย์ได้ที่ tailscale.com/admin/settings/keys หรือจากอินสแตนซ์ Headscale ของคุณ",
|
||||
"tailscaleApiKeyDocsLink": "ดูเอกสาร",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "โปรโตคอล",
|
||||
"domainLabel": "โดเมน",
|
||||
"domainPlaceholder": "ไม่จำเป็น",
|
||||
"connectToRdp": "เชื่อมต่อผ่าน RDP",
|
||||
"connectToVnc": "เชื่อมต่อผ่าน VNC",
|
||||
"connectToTerminal": "เชื่อมต่อกับเทอร์มินัล",
|
||||
"connectToFiles": "เชื่อมต่อกับไฟล์"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "กำหนดค่าต่างๆ ที่โฮสต์ทุกตัวจะได้รับสืบทอด เว้นแต่ว่าโฮสต์นั้นจะกำหนดค่าใหม่เอง",
|
||||
"manage": "จัดการ",
|
||||
"dialogDescription": "ข้อกำหนดเหล่านี้ใช้กับโฮสต์ที่ไม่ได้ตั้งค่าใดๆ ไว้ การตั้งค่าใดๆ บนโฮสต์จะมีผลเหนือกว่า",
|
||||
"tabTerminal": "เทอร์มินัล",
|
||||
"tabRemoteDesktop": "รีโมทเดสก์ท็อป",
|
||||
"inherit": "สืบทอด",
|
||||
"on": "บน",
|
||||
"off": "ปิด",
|
||||
"resizeDisplayUpdate": "อัปเดตการแสดงผล",
|
||||
"resizeReconnect": "เชื่อมต่อใหม่",
|
||||
"clearAll": "ล้างทั้งหมด",
|
||||
"saved": "บันทึกการตั้งค่าการเชื่อมต่อเริ่มต้นแล้ว",
|
||||
"saveFailed": "ไม่สามารถบันทึกการตั้งค่าการเชื่อมต่อเริ่มต้นได้"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Bağlantı Ayrıntıları",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Uzak Masaüstü",
|
||||
"port": "Bağlantı Noktası",
|
||||
"username": "Kullanıcı Adı",
|
||||
"folder": "Klasör",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Genel",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Sunucu Metrikleri",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Paylaşım",
|
||||
"tabAuthentication": "Kimlik Doğrulama",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Tüm Seçimi Kaldır",
|
||||
"protocols": "Protokoller",
|
||||
"secureShell": "Secure Shell",
|
||||
"virtualNetwork": "Sanal Ağ",
|
||||
"unencryptedShell": "Şifrelenmemiş Shell",
|
||||
"addressIp": "Adres / IP",
|
||||
"friendlyName": "Kolay Ad",
|
||||
"macAddress": "MAC Adresi",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Adım CA",
|
||||
"filterProtocolGroup": "Protokol",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Özellikler",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Dosya Yöneticisi",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Doğrudan",
|
||||
"authTypeCredential": "Kimlik bilgisi",
|
||||
"authTypeNone": "Hiçbiri",
|
||||
"colorDepth": "Renk Derinliği",
|
||||
"resizeMethod": "Yeniden Boyutlandırma Yöntemi",
|
||||
"recordingPath": "Kayıt Yolu",
|
||||
"recordingName": "Kayıt Adı",
|
||||
"saveHostFirst": "Önce ana makineyi kaydedin.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Guacamole'yi Etkinleştir",
|
||||
"enableGuacamoleDesc": "RDP/VNC uzak masaüstü",
|
||||
"enableGuacamoleDocsLink": "Belgeleri görüntüle",
|
||||
"guacdUrl": "guacd URL'si",
|
||||
"tailscaleApiKey": "Tailscale API Anahtarı",
|
||||
"tailscaleApiKeyDescription": "Ana bilgisayar düzenleyicisinde cihaz keşfi için kullanılır. Tailscale (tskey-) ve Headscale (hskey-) anahtarlarını destekler. Anahtarınızı tailscale.com/admin/settings/keys adresinden veya Headscale örneğinizden oluşturabilirsiniz.",
|
||||
"tailscaleApiKeyDocsLink": "Belgeleri görüntüle",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Protokol",
|
||||
"domainLabel": "İhtisas",
|
||||
"domainPlaceholder": "isteğe bağlı",
|
||||
"connectToRdp": "RDP üzerinden bağlanın",
|
||||
"connectToVnc": "VNC üzerinden bağlantı kurun",
|
||||
"connectToTerminal": "Terminale Bağlan",
|
||||
"connectToFiles": "Dosyalara Bağlan"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Sunucunun kendisi tarafından geçersiz kılınmadığı sürece her sunucunun devralacağı değerler belirleyin.",
|
||||
"manage": "Üstesinden gelmek",
|
||||
"dialogDescription": "Bu kurallar, ayarlarını değiştirmeyen sunucular için geçerlidir. Sunucuda yapılan herhangi bir ayar geçerlidir.",
|
||||
"tabTerminal": "terminal",
|
||||
"tabRemoteDesktop": "Uzaktan Masaüstü",
|
||||
"inherit": "Miras almak",
|
||||
"on": "Açık",
|
||||
"off": "Kapalı",
|
||||
"resizeDisplayUpdate": "Ekran Güncellemesi",
|
||||
"resizeReconnect": "Yeniden bağlan",
|
||||
"clearAll": "Tümünü Temizle",
|
||||
"saved": "Bağlantı varsayılan ayarları kaydedildi.",
|
||||
"saveFailed": "Bağlantı varsayılan ayarları kaydedilemedi."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Деталі підключення",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Телнет",
|
||||
"remoteDesktop": "Віддалений робочий стіл",
|
||||
"port": "Порт",
|
||||
"username": "Ім'я користувача",
|
||||
"folder": "Папка",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Загальне",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Термінал",
|
||||
"tabRdp": "ПРП",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Метрики хоста",
|
||||
"tabTelnet": "Телнет",
|
||||
"tabSharing": "Спільне використання",
|
||||
"tabAuthentication": "Автентифікація",
|
||||
"terminal": "Термінал",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Зняти вибір усіх",
|
||||
"protocols": "Протоколи",
|
||||
"secureShell": "Безпечна оболонка",
|
||||
"virtualNetwork": "Віртуальна мережа",
|
||||
"unencryptedShell": "Незашифрована оболонка",
|
||||
"addressIp": "Адреса / IP-адреса",
|
||||
"friendlyName": "Зрозуміле ім'я",
|
||||
"macAddress": "MAC-адреса",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Крок CA",
|
||||
"filterProtocolGroup": "Протокол",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "ПРП",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Телнет",
|
||||
"filterFeaturesGroup": "Особливості",
|
||||
"filterFeatureTerminal": "Термінал",
|
||||
"filterFeatureFileManager": "Файловий менеджер",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Прямий",
|
||||
"authTypeCredential": "Посвідчення",
|
||||
"authTypeNone": "Жоден",
|
||||
"colorDepth": "Глибина кольору",
|
||||
"resizeMethod": "Метод зміни розміру",
|
||||
"recordingPath": "Шлях запису",
|
||||
"recordingName": "Назва запису",
|
||||
"saveHostFirst": "Спочатку збережіть хост.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Увімкнути гуакамоле",
|
||||
"enableGuacamoleDesc": "Віддалений робочий стіл RDP/VNC",
|
||||
"enableGuacamoleDocsLink": "Переглянути документи",
|
||||
"guacdUrl": "URL-адреса GUACD",
|
||||
"tailscaleApiKey": "Ключ API Tailscale",
|
||||
"tailscaleApiKeyDescription": "Використовується для виявлення пристроїв у редакторі хоста. Підтримує ключі Tailscale (tkey-) та Headscale (hskey-). Згенеруйте ключ на tailscale.com/admin/settings/keys або з вашого екземпляра Headscale.",
|
||||
"tailscaleApiKeyDocsLink": "Переглянути документи",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Протокол",
|
||||
"domainLabel": "Домен",
|
||||
"domainPlaceholder": "необов'язково",
|
||||
"connectToRdp": "Підключення через RDP",
|
||||
"connectToVnc": "Підключення через VNC",
|
||||
"connectToTerminal": "Підключення до терміналу",
|
||||
"connectToFiles": "Підключення до файлів"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Встановіть значення, які успадковує кожен хост, якщо він їх не перевизначить.",
|
||||
"manage": "Керувати",
|
||||
"dialogDescription": "Це стосується хостів, які залишають налаштування невизначеними. Будь-яке налаштування, встановлене на хості, виграє.",
|
||||
"tabTerminal": "Термінал",
|
||||
"tabRemoteDesktop": "Віддалений робочий стіл",
|
||||
"inherit": "Успадкувати",
|
||||
"on": "Увімкнено",
|
||||
"off": "Вимкнено",
|
||||
"resizeDisplayUpdate": "Оновлення дисплея",
|
||||
"resizeReconnect": "Знову підключитися",
|
||||
"clearAll": "Очистити все",
|
||||
"saved": "Налаштування з'єднання за замовчуванням збережено",
|
||||
"saveFailed": "Не вдалося зберегти налаштування з’єднання за замовчуванням"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "Chi tiết kết nối",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "Máy tính từ xa",
|
||||
"port": "Cổng",
|
||||
"username": "Tên đăng nhập",
|
||||
"folder": "Thư mục",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "Chung",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "Terminal",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "Số liệu máy chủ",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "Chia sẻ",
|
||||
"tabAuthentication": "Xác thực",
|
||||
"terminal": "Terminal",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "Bỏ chọn tất cả",
|
||||
"protocols": "Giao thức",
|
||||
"secureShell": "Secure Shell",
|
||||
"virtualNetwork": "Mạng ảo",
|
||||
"unencryptedShell": "Shell không mã hóa",
|
||||
"addressIp": "Địa chỉ / IP",
|
||||
"friendlyName": "Tên hiển thị",
|
||||
"macAddress": "Địa chỉ MAC",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "Bước CA",
|
||||
"filterProtocolGroup": "Giao thức",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "Tính năng",
|
||||
"filterFeatureTerminal": "Terminal",
|
||||
"filterFeatureFileManager": "Trình quản lý file",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "Trực tiếp",
|
||||
"authTypeCredential": "Thông tin xác thực",
|
||||
"authTypeNone": "Không có",
|
||||
"colorDepth": "Độ sâu màu",
|
||||
"resizeMethod": "Phương thức thay đổi kích thước",
|
||||
"recordingPath": "Đường dẫn ghi",
|
||||
"recordingName": "Tên bản ghi",
|
||||
"saveHostFirst": "Lưu máy chủ trước.",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "Bật Guacamole",
|
||||
"enableGuacamoleDesc": "Kết nối RDP/VNC từ xa",
|
||||
"enableGuacamoleDocsLink": "Xem tài liệu",
|
||||
"guacdUrl": "URL guacd",
|
||||
"tailscaleApiKey": "Khóa API Tailscale",
|
||||
"tailscaleApiKeyDescription": "Được sử dụng để phát hiện thiết bị trong trình chỉnh sửa máy chủ. Hỗ trợ khóa Tailscale (tskey-) và Headscale (hskey-). Tạo khóa tại tailscale.com/admin/settings/keys hoặc từ phiên bản Headscale của bạn.",
|
||||
"tailscaleApiKeyDocsLink": "Xem tài liệu",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "Giao thức",
|
||||
"domainLabel": "Lãnh địa",
|
||||
"domainPlaceholder": "không bắt buộc",
|
||||
"connectToRdp": "Kết nối qua RDP",
|
||||
"connectToVnc": "Kết nối qua VNC",
|
||||
"connectToTerminal": "Kết nối đến Terminal",
|
||||
"connectToFiles": "Kết nối đến Files"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "Đặt các giá trị mà mọi máy chủ đều kế thừa trừ khi nó ghi đè lên các giá trị đó.",
|
||||
"manage": "Quản lý",
|
||||
"dialogDescription": "Các quy định này áp dụng cho các máy chủ không thiết lập bất kỳ cài đặt nào. Bất kỳ cài đặt nào được thiết lập trên máy chủ đều sẽ được ưu tiên.",
|
||||
"tabTerminal": "Phần cuối",
|
||||
"tabRemoteDesktop": "Máy tính từ xa",
|
||||
"inherit": "Kế thừa",
|
||||
"on": "TRÊN",
|
||||
"off": "Tắt",
|
||||
"resizeDisplayUpdate": "Cập nhật hiển thị",
|
||||
"resizeReconnect": "Kết nối lại",
|
||||
"clearAll": "Xóa tất cả",
|
||||
"saved": "Cài đặt kết nối mặc định đã được lưu",
|
||||
"saveFailed": "Không thể lưu cài đặt kết nối mặc định."
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "连接详情",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "远程桌面",
|
||||
"port": "端口",
|
||||
"username": "用户名",
|
||||
"folder": "文件夹",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "常规",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "终端",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "主机指标",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "共享",
|
||||
"tabAuthentication": "认证",
|
||||
"terminal": "终端",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "取消全选",
|
||||
"protocols": "协议",
|
||||
"secureShell": "SSH",
|
||||
"virtualNetwork": "虚拟网络",
|
||||
"unencryptedShell": "未加密 Shell",
|
||||
"addressIp": "地址 / IP",
|
||||
"friendlyName": "友好名称",
|
||||
"macAddress": "MAC 地址",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "步骤 CA",
|
||||
"filterProtocolGroup": "协议",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "功能",
|
||||
"filterFeatureTerminal": "终端",
|
||||
"filterFeatureFileManager": "文件管理器",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "直接",
|
||||
"authTypeCredential": "凭据",
|
||||
"authTypeNone": "无",
|
||||
"colorDepth": "颜色深度",
|
||||
"resizeMethod": "调整大小方法",
|
||||
"recordingPath": "录制路径",
|
||||
"recordingName": "录制名称",
|
||||
"saveHostFirst": "请先保存主机。",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "启用 Guacamole",
|
||||
"enableGuacamoleDesc": "RDP/VNC 远程桌面",
|
||||
"enableGuacamoleDocsLink": "查看文档",
|
||||
"guacdUrl": "guacd URL",
|
||||
"tailscaleApiKey": "Tailscale API 密钥",
|
||||
"tailscaleApiKeyDescription": "用于主机编辑器中的设备发现。支持 Tailscale (tskey-) 和 Headscale (hskey-) 密钥。您可以在 tailscale.com/admin/settings/keys 或从您的 Headscale 实例生成密钥。",
|
||||
"tailscaleApiKeyDocsLink": "查看文档",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "协议",
|
||||
"domainLabel": "领域",
|
||||
"domainPlaceholder": "选修的",
|
||||
"connectToRdp": "通过 RDP 连接",
|
||||
"connectToVnc": "通过 VNC 连接",
|
||||
"connectToTerminal": "连接到终端",
|
||||
"connectToFiles": "连接到文件管理器"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "设置所有主机都将继承的值,除非主机覆盖这些值。",
|
||||
"manage": "管理",
|
||||
"dialogDescription": "这些规则适用于未设置相关设置的主机。主机上已设置的任何内容均会生效。",
|
||||
"tabTerminal": "终端",
|
||||
"tabRemoteDesktop": "远程桌面",
|
||||
"inherit": "继承",
|
||||
"on": "开启",
|
||||
"off": "关闭",
|
||||
"resizeDisplayUpdate": "显示更新",
|
||||
"resizeReconnect": "重新连接",
|
||||
"clearAll": "全部清除",
|
||||
"saved": "已保存连接默认设置",
|
||||
"saveFailed": "保存连接默认设置失败"
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"connectionDetails": "連線詳細資料",
|
||||
"ssh": "SSH",
|
||||
"telnet": "Telnet",
|
||||
"remoteDesktop": "遠端桌面",
|
||||
"port": "連接埠",
|
||||
"username": "使用者名稱",
|
||||
"folder": "資料夾",
|
||||
@@ -756,10 +755,7 @@
|
||||
"tabGeneral": "一般",
|
||||
"tabSsh": "SSH",
|
||||
"tabTerminal": "終端機",
|
||||
"tabRdp": "RDP",
|
||||
"tabVnc": "VNC",
|
||||
"tabStats": "主機指標",
|
||||
"tabTelnet": "Telnet",
|
||||
"tabSharing": "分享",
|
||||
"tabAuthentication": "驗證",
|
||||
"terminal": "終端機",
|
||||
@@ -833,8 +829,6 @@
|
||||
"deselectAll": "取消全選",
|
||||
"protocols": "通訊協定",
|
||||
"secureShell": "安全殼層",
|
||||
"virtualNetwork": "虛擬網路",
|
||||
"unencryptedShell": "未加密殼層",
|
||||
"addressIp": "位址 / IP",
|
||||
"friendlyName": "易記名稱",
|
||||
"macAddress": "MAC 位址",
|
||||
@@ -1381,9 +1375,6 @@
|
||||
"filterAuthStepca": "步驟 CA",
|
||||
"filterProtocolGroup": "通訊協定",
|
||||
"filterProtocolSsh": "SSH",
|
||||
"filterProtocolRdp": "RDP",
|
||||
"filterProtocolVnc": "VNC",
|
||||
"filterProtocolTelnet": "Telnet",
|
||||
"filterFeaturesGroup": "功能",
|
||||
"filterFeatureTerminal": "終端機",
|
||||
"filterFeatureFileManager": "檔案管理員",
|
||||
@@ -1482,8 +1473,6 @@
|
||||
"authTypeDirect": "直接",
|
||||
"authTypeCredential": "憑證",
|
||||
"authTypeNone": "沒有任何",
|
||||
"colorDepth": "色彩深度",
|
||||
"resizeMethod": "調整尺寸方法",
|
||||
"recordingPath": "錄製路徑",
|
||||
"recordingName": "錄製名稱",
|
||||
"saveHostFirst": "請先儲存主機。",
|
||||
@@ -2108,7 +2097,6 @@
|
||||
"enableGuacamole": "啟用 Guacamole",
|
||||
"enableGuacamoleDesc": "RDP/VNC 遠端桌面",
|
||||
"enableGuacamoleDocsLink": "檢視文件",
|
||||
"guacdUrl": "guacd URL",
|
||||
"tailscaleApiKey": "Tailscale API 金鑰",
|
||||
"tailscaleApiKeyDescription": "用於主機編輯器中的設備發現。支援 Tailscale (tskey-) 和 Headscale (hskey-) 金鑰。您可以在 tailscale.com/admin/settings/keys 或從您的 Headscale 實例產生金鑰。",
|
||||
"tailscaleApiKeyDocsLink": "檢視文件",
|
||||
@@ -2424,8 +2412,6 @@
|
||||
"protocolLabel": "協定",
|
||||
"domainLabel": "領域",
|
||||
"domainPlaceholder": "選修的",
|
||||
"connectToRdp": "透過 RDP 連接",
|
||||
"connectToVnc": "透過 VNC 連接",
|
||||
"connectToTerminal": "連線至終端機",
|
||||
"connectToFiles": "連線至檔案管理員"
|
||||
},
|
||||
@@ -2633,13 +2619,9 @@
|
||||
"description": "設定所有主機都將繼承的值,除非主機覆寫這些值。",
|
||||
"manage": "管理",
|
||||
"dialogDescription": "這些規則適用於未設定相關設定的主機。主機上已設定的任何內容均會生效。",
|
||||
"tabTerminal": "終端",
|
||||
"tabRemoteDesktop": "遠端桌面",
|
||||
"inherit": "繼承",
|
||||
"on": "在",
|
||||
"off": "離開",
|
||||
"resizeDisplayUpdate": "顯示更新",
|
||||
"resizeReconnect": "重新連接",
|
||||
"clearAll": "全部清除",
|
||||
"saved": "已儲存連線預設設定",
|
||||
"saveFailed": "儲存連線預設設定失敗"
|
||||
|
||||
+1
-11
@@ -794,7 +794,7 @@ function getApiUrl(path: string, defaultPort: number): string {
|
||||
// JWT fresh on every request (cheap, and correct even if the user
|
||||
// connects/disconnects remote sync without an app reload).
|
||||
|
||||
function createRemoteOriginApiInstance(path: string): AxiosInstance {
|
||||
export function createRemoteOriginApiInstance(path: string): AxiosInstance {
|
||||
const instance = axios.create({
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 30000,
|
||||
@@ -835,7 +835,6 @@ function createRemoteOriginApiInstance(path: string): AxiosInstance {
|
||||
let remoteFileManagerApi: AxiosInstance | null = null;
|
||||
let remoteStatsApi: AxiosInstance | null = null;
|
||||
let remoteCoreApi: AxiosInstance | null = null;
|
||||
let remoteGuacamoleApi: AxiosInstance | null = null;
|
||||
|
||||
export function getRemoteFileManagerApi(): AxiosInstance {
|
||||
if (!remoteFileManagerApi) {
|
||||
@@ -867,13 +866,6 @@ export function getRemoteCoreApi(): AxiosInstance {
|
||||
return remoteCoreApi;
|
||||
}
|
||||
|
||||
export function getRemoteGuacamoleApi(): AxiosInstance {
|
||||
if (!remoteGuacamoleApi) {
|
||||
remoteGuacamoleApi = createRemoteOriginApiInstance("");
|
||||
}
|
||||
return remoteGuacamoleApi;
|
||||
}
|
||||
|
||||
// Maps a live SSH session (keyed by sessionId, which today is the host's
|
||||
// numeric id as a string -- see ensureSSHSessionForHost) to the resolved
|
||||
// origin it was connected through, so every subsequent file-manager call
|
||||
@@ -1582,8 +1574,6 @@ export {
|
||||
updateLogLevel,
|
||||
getSessionTimeout,
|
||||
updateSessionTimeout,
|
||||
getGuacamoleSettings,
|
||||
updateGuacamoleSettings,
|
||||
} from "@/api/settings-api";
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
registerHostContextMenuItem,
|
||||
} from "@/sidebar/host-contributions";
|
||||
import { registerPaletteEntry } from "@/shell/palette-registry";
|
||||
import { registerHostProtocol } from "@/sidebar/host-protocols";
|
||||
import {
|
||||
registerDashboardCard,
|
||||
type DashboardCardRenderProps,
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
registerAction,
|
||||
registerSlotContribution,
|
||||
} from "@/shell/action-registry";
|
||||
import { pluginWsUrl } from "@/lib/plugin-transport";
|
||||
import { pluginApiFor, pluginWsUrl } from "@/lib/plugin-transport";
|
||||
import { registerPluginComponent } from "./component-registry";
|
||||
import type { WidgetTypeDefinition } from "@/types/homepage-types";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
@@ -208,6 +209,19 @@ export function createPluginApp(
|
||||
);
|
||||
},
|
||||
|
||||
registerHostProtocol(protocol) {
|
||||
return track(
|
||||
registerHostProtocol({
|
||||
...protocol,
|
||||
pluginId,
|
||||
titleKey: key(protocol.titleKey),
|
||||
descriptionKey: protocol.descriptionKey
|
||||
? key(protocol.descriptionKey)
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
registerHostBadge(badge) {
|
||||
return track(
|
||||
registerHostBadge({
|
||||
@@ -367,6 +381,12 @@ export function createPluginApp(
|
||||
}
|
||||
},
|
||||
api: pluginHostBridge.getApi(pluginId),
|
||||
apiFor: (origin) =>
|
||||
pluginApiFor(
|
||||
pluginId,
|
||||
origin as never,
|
||||
pluginHostBridge.getApi(pluginId) as never,
|
||||
) as unknown as TermixApp["api"],
|
||||
wsUrl: (path, options) =>
|
||||
pluginWsUrl(pluginId, path, options as never) as ReturnType<
|
||||
TermixApp["wsUrl"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { enabledHostProtocols } from "@/sidebar/host-protocols";
|
||||
import { useTranslation as useI18nTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -275,6 +276,13 @@ export const pluginHostBridge: PluginHostBridge = {
|
||||
|
||||
useSlotContributions: (slotId, context) => useActionSlot(slotId, context),
|
||||
|
||||
hostProtocols: (record) => [
|
||||
...(record.enableSsh !== false ? ["ssh"] : []),
|
||||
...enabledHostProtocols(
|
||||
record as { pluginSettings?: Record<string, Record<string, unknown>> },
|
||||
).map((protocol) => protocol.id),
|
||||
],
|
||||
|
||||
usePluginComponent: (id) => usePluginComponent(id),
|
||||
};
|
||||
|
||||
|
||||
@@ -112,7 +112,16 @@ export {
|
||||
type ConnectionOrigin,
|
||||
} from "@/lib/connection-origin";
|
||||
export { pluginWsUrl } from "@/lib/plugin-transport";
|
||||
export { hydrateLocalSharedHostAuth } from "@/lib/remote-server-api";
|
||||
export {
|
||||
buildOriginWsUrl,
|
||||
type WebSocketConnectionTarget,
|
||||
} from "@/lib/connection-origin";
|
||||
export { getBasePath } from "@/lib/base-path";
|
||||
export type { ConnectionStage } from "@/types/connection-log";
|
||||
export {
|
||||
hydrateLocalSharedHostAuth,
|
||||
resolveRemoteHostId,
|
||||
} from "@/lib/remote-server-api";
|
||||
export { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext";
|
||||
|
||||
// Core APIs a terminal-like surface calls: recent activity, the host's stored
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { getPanel, listPanels } from "@/shell/panel-registry";
|
||||
import { listRegisteredRailItems } from "@/sidebar/rail-items";
|
||||
import { listHostActions } from "@/sidebar/host-contributions";
|
||||
import { listHostProtocols } from "@/sidebar/host-protocols";
|
||||
import {
|
||||
getHostEditorSection,
|
||||
hostEditorSectionList,
|
||||
@@ -207,6 +208,8 @@ export async function renderPlugin(
|
||||
id: action.id,
|
||||
tabType: action.tabType,
|
||||
})),
|
||||
hostProtocols: () =>
|
||||
mine(listHostProtocols()).map((protocol) => protocol.id),
|
||||
hostEditorSections: () =>
|
||||
mine(hostEditorSectionList()).map((section) => section.id),
|
||||
dashboardCards: () =>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { SectionCard } from "@/components/section-card";
|
||||
import { PluginIcon } from "@/lib/plugin-icon";
|
||||
import { getPlugins, type PluginSummary } from "@/api/plugins-api";
|
||||
import { SettingsFieldRow } from "./SettingsFields";
|
||||
import { isFieldActive } from "./settings-fields-util";
|
||||
import { hasVisibleFields, isFieldActive } from "./settings-fields-util";
|
||||
|
||||
/** Values for every plugin on one host: { [pluginId]: { [key]: value } }. */
|
||||
export type HostPluginSettings = Record<string, Record<string, unknown>>;
|
||||
@@ -45,7 +45,7 @@ export function usePluginHostSections(): PluginSummary[] {
|
||||
plugins.filter((plugin) => {
|
||||
if (!plugin.enabled) return false;
|
||||
const host = plugin.contributes?.settings?.host;
|
||||
return !!host && (host.fields.length > 0 || !!host.enableKey);
|
||||
return !!host && (hasVisibleFields(host.fields) || !!host.enableKey);
|
||||
}),
|
||||
[plugins],
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
HostPluginSections,
|
||||
type HostPluginSettings,
|
||||
} from "./HostPluginSections";
|
||||
import { hasVisibleFields } from "./settings-fields-util";
|
||||
|
||||
export const HOST_PLUGINS_TAB_ID = "plugins";
|
||||
|
||||
@@ -38,7 +39,7 @@ export function syncHostPluginsTab(plugins: PluginSummary[]): boolean {
|
||||
const contributors = plugins.filter((plugin) => {
|
||||
if (!plugin.enabled) return false;
|
||||
const host = plugin.contributes?.settings?.host;
|
||||
return !!host && (host.fields.length > 0 || !!host.enableKey);
|
||||
return !!host && (hasVisibleFields(host.fields) || !!host.enableKey);
|
||||
});
|
||||
|
||||
if (contributors.length === 0) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { PluginSettingsField } from "@/api/plugins-api";
|
||||
|
||||
/**
|
||||
* Whether a field's `requires` gate is satisfied.
|
||||
* Whether the generic form draws a field: not hidden (the plugin edits it in
|
||||
* its own UI) and its `requires` gate satisfied.
|
||||
*
|
||||
* Kept out of the component file so both the settings page and the host editor
|
||||
* section can import it without dragging a component along.
|
||||
@@ -10,6 +11,12 @@ export function isFieldActive(
|
||||
field: PluginSettingsField,
|
||||
values: Record<string, unknown>,
|
||||
): boolean {
|
||||
if (field.hidden) return false;
|
||||
if (!field.requires) return true;
|
||||
return values[field.requires] === true;
|
||||
}
|
||||
|
||||
/** Whether a plugin has any field the generic form would ever draw. */
|
||||
export function hasVisibleFields(fields: PluginSettingsField[]): boolean {
|
||||
return fields.some((field) => !field.hidden);
|
||||
}
|
||||
@@ -33,8 +33,6 @@ import {
|
||||
updateGlobalMonitoringSettings,
|
||||
getLogLevel,
|
||||
updateLogLevel,
|
||||
getGuacamoleSettings,
|
||||
updateGuacamoleSettings,
|
||||
getOidcAutoProvision,
|
||||
updateOidcAutoProvision,
|
||||
getSecondFactorAfterExternalLogin,
|
||||
@@ -137,8 +135,6 @@ export function AdminSettingsPanel({
|
||||
const [sessionTimeout, setSessionTimeout] = useState("24");
|
||||
const [statusInterval, setStatusInterval] = useState("60");
|
||||
const [metricsInterval, setMetricsInterval] = useState("30");
|
||||
const [guacEnabled, setGuacEnabled] = useState(false);
|
||||
const [guacUrl, setGuacUrl] = useState("guacd:4822");
|
||||
const [logLevel, setLogLevel] = useState("info");
|
||||
const [analyticsEnabled, setAnalyticsEnabled] = useState(true);
|
||||
const [analyticsLocked, setAnalyticsLocked] = useState(false);
|
||||
@@ -350,7 +346,6 @@ export function AdminSettingsPanel({
|
||||
timeout,
|
||||
monitoring,
|
||||
level,
|
||||
guac,
|
||||
oidcProv,
|
||||
secondFactorExternal,
|
||||
oidcSilent,
|
||||
@@ -366,7 +361,6 @@ export function AdminSettingsPanel({
|
||||
getSessionTimeout(),
|
||||
getGlobalMonitoringSettings(),
|
||||
getLogLevel(),
|
||||
getGuacamoleSettings(),
|
||||
getOidcAutoProvision(),
|
||||
getSecondFactorAfterExternalLogin(),
|
||||
getOidcSilentLoginDefault(),
|
||||
@@ -402,10 +396,6 @@ export function AdminSettingsPanel({
|
||||
}
|
||||
|
||||
if (level.status === "fulfilled") setLogLevel(level.value.level);
|
||||
if (guac.status === "fulfilled") {
|
||||
setGuacEnabled(guac.value.enabled);
|
||||
setGuacUrl(guac.value.url || "guacd:4822");
|
||||
}
|
||||
if (analytics.status === "fulfilled") {
|
||||
setAnalyticsEnabled(analytics.value.enabled);
|
||||
setAnalyticsLocked(analytics.value.locked ?? false);
|
||||
@@ -656,26 +646,6 @@ export function AdminSettingsPanel({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveGuacamole() {
|
||||
try {
|
||||
await updateGuacamoleSettings({ enabled: guacEnabled, url: guacUrl });
|
||||
toast.success(t("admin.remoteDesktopSaved"));
|
||||
} catch {
|
||||
toast.error(t("admin.remoteDesktopSaveFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleGuacamole() {
|
||||
const newVal = !guacEnabled;
|
||||
setGuacEnabled(newVal);
|
||||
try {
|
||||
await updateGuacamoleSettings({ enabled: newVal, url: guacUrl });
|
||||
} catch {
|
||||
setGuacEnabled(!newVal);
|
||||
toast.error(t("admin.remoteDesktopUpdateFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveLogLevel(level: string) {
|
||||
setLogLevel(level);
|
||||
try {
|
||||
@@ -1103,11 +1073,6 @@ export function AdminSettingsPanel({
|
||||
metricsInterval={metricsInterval}
|
||||
setMetricsInterval={setMetricsInterval}
|
||||
handleSaveMonitoring={handleSaveMonitoring}
|
||||
guacEnabled={guacEnabled}
|
||||
handleToggleGuacamole={handleToggleGuacamole}
|
||||
guacUrl={guacUrl}
|
||||
setGuacUrl={setGuacUrl}
|
||||
handleSaveGuacamole={handleSaveGuacamole}
|
||||
logLevel={logLevel}
|
||||
handleSaveLogLevel={handleSaveLogLevel}
|
||||
/>
|
||||
|
||||
@@ -60,11 +60,6 @@ type GeneralSettingsSectionProps = {
|
||||
metricsInterval: string;
|
||||
setMetricsInterval: Dispatch<SetStateAction<string>>;
|
||||
handleSaveMonitoring: () => void;
|
||||
guacEnabled: boolean;
|
||||
handleToggleGuacamole: () => void;
|
||||
guacUrl: string;
|
||||
setGuacUrl: Dispatch<SetStateAction<string>>;
|
||||
handleSaveGuacamole: () => void;
|
||||
logLevel: string;
|
||||
handleSaveLogLevel: (level: string) => void;
|
||||
};
|
||||
@@ -106,11 +101,6 @@ export function AdminGeneralSettingsSection({
|
||||
metricsInterval,
|
||||
setMetricsInterval,
|
||||
handleSaveMonitoring,
|
||||
guacEnabled,
|
||||
handleToggleGuacamole,
|
||||
guacUrl,
|
||||
setGuacUrl,
|
||||
handleSaveGuacamole,
|
||||
logLevel,
|
||||
handleSaveLogLevel,
|
||||
}: GeneralSettingsSectionProps) {
|
||||
@@ -390,50 +380,6 @@ export function AdminGeneralSettingsSection({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3 mt-2">
|
||||
<SettingRow
|
||||
label={t("admin.enableRemoteDesktop")}
|
||||
description={
|
||||
<>
|
||||
{t("admin.enableRemoteDesktopDesc")}{" "}
|
||||
<a
|
||||
href="https://docs.termix.site/setup/remote-desktop"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-accent-brand hover:underline"
|
||||
>
|
||||
{t("admin.enableRemoteDesktopDocsLink")}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<AdminToggle on={guacEnabled} onToggle={handleToggleGuacamole} />
|
||||
</SettingRow>
|
||||
{guacEnabled && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.guacdUrl")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={guacUrl}
|
||||
onChange={(e) => setGuacUrl(e.target.value)}
|
||||
placeholder="guacd:4822"
|
||||
className="text-sm"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand h-7 shrink-0"
|
||||
onClick={handleSaveGuacamole}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3 mt-2">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("admin.logLevel")}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { hostProtocolFlags, type HostProtocols } from "./host-protocols";
|
||||
import { getUserList } from "@/main-axios";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
@@ -141,12 +142,9 @@ export function AdminUserManagePanel({
|
||||
}, [user.id]);
|
||||
const [editor, setEditor] = useState<EditorState>(null);
|
||||
const [editorTab, setEditorTab] = useState("general");
|
||||
const [editorProtocols, setEditorProtocols] = useState({
|
||||
enableSsh: true,
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
});
|
||||
const [editorProtocols, setEditorProtocols] = useState<HostProtocols>(() =>
|
||||
hostProtocolFlags(null),
|
||||
);
|
||||
const [confirmDialog, setConfirmDialog] = useState<{
|
||||
message: string;
|
||||
onConfirm: () => void;
|
||||
@@ -791,12 +789,7 @@ export function AdminUserManagePanel({
|
||||
size="sm"
|
||||
className="h-6 text-[10px] border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
|
||||
onClick={() => {
|
||||
setEditorProtocols({
|
||||
enableSsh: true,
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
});
|
||||
setEditorProtocols(hostProtocolFlags(null));
|
||||
setEditorTab("general");
|
||||
setEditor({ kind: "host", host: null });
|
||||
}}
|
||||
@@ -842,12 +835,7 @@ export function AdminUserManagePanel({
|
||||
size="icon"
|
||||
className="size-6 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
setEditorProtocols({
|
||||
enableSsh: host.enableSsh,
|
||||
enableRdp: host.enableRdp,
|
||||
enableVnc: host.enableVnc,
|
||||
enableTelnet: host.enableTelnet,
|
||||
});
|
||||
setEditorProtocols(hostProtocolFlags(host));
|
||||
setEditorTab("general");
|
||||
setEditor({ kind: "host", host });
|
||||
}}
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import {
|
||||
fromTriState,
|
||||
toTriState,
|
||||
type RemoteDesktopDefaults,
|
||||
type TerminalDefaults,
|
||||
type TriState,
|
||||
} from "@/lib/connection-defaults";
|
||||
@@ -35,34 +34,6 @@ const inputClass =
|
||||
const labelClass =
|
||||
"text-[10px] font-bold uppercase tracking-widest text-muted-foreground";
|
||||
|
||||
const RDP_TOGGLES = [
|
||||
["forceLossless", "hosts.guac.forceLossless", "hosts.guac.forceLosslessDesc"],
|
||||
["enableWallpaper", "hosts.guac.wallpaper", "hosts.guac.wallpaperDesc"],
|
||||
[
|
||||
"enableFontSmoothing",
|
||||
"hosts.guac.fontSmoothing",
|
||||
"hosts.guac.fontSmoothingDesc",
|
||||
],
|
||||
[
|
||||
"enableDesktopComposition",
|
||||
"hosts.guac.desktopComposition",
|
||||
"hosts.guac.desktopCompositionDesc",
|
||||
],
|
||||
["disableAudio", "hosts.guac.disableAudio", "hosts.guac.disableAudioDesc"],
|
||||
[
|
||||
"enablePrinting",
|
||||
"hosts.guac.enablePrinting",
|
||||
"hosts.guac.enablePrintingDesc",
|
||||
],
|
||||
[
|
||||
"enableDrive",
|
||||
"hosts.guac.enableDriveRedirection",
|
||||
"hosts.guac.enableDriveRedirectionDesc",
|
||||
],
|
||||
["disableCopy", "hosts.guac.disableCopy", "hosts.guac.disableCopyDesc"],
|
||||
["disablePaste", "hosts.guac.disablePaste", "hosts.guac.disablePasteDesc"],
|
||||
] as const;
|
||||
|
||||
function Field({
|
||||
label,
|
||||
children,
|
||||
@@ -105,17 +76,14 @@ export function ConnectionDefaultsSettings() {
|
||||
const { t } = useTranslation();
|
||||
const defaults = useConnectionDefaults();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [tab, setTab] = useState<"shell" | "desktop">("shell");
|
||||
const [terminal, setTerminal] = useState<TerminalDefaults>({});
|
||||
const [rdp, setRdp] = useState<RemoteDesktopDefaults>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [savedThemes, setSavedThemes] = useState<SavedCustomTheme[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaults.ready) return;
|
||||
setTerminal(defaults.terminal);
|
||||
setRdp(defaults.rdp);
|
||||
}, [defaults.ready, defaults.terminal, defaults.rdp]);
|
||||
}, [defaults.ready, defaults.terminal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -127,17 +95,13 @@ export function ConnectionDefaultsSettings() {
|
||||
const configuredCount = useMemo(() => {
|
||||
const count = (source: Record<string, unknown>) =>
|
||||
Object.values(source).filter((value) => value !== undefined).length;
|
||||
return count(defaults.terminal) + count(defaults.rdp);
|
||||
}, [defaults.terminal, defaults.rdp]);
|
||||
return count(defaults.terminal);
|
||||
}, [defaults.terminal]);
|
||||
|
||||
const updateTerminal = <K extends keyof TerminalDefaults>(
|
||||
key: K,
|
||||
value: TerminalDefaults[K],
|
||||
) => setTerminal((current) => ({ ...current, [key]: value }));
|
||||
const updateRdp = <K extends keyof RemoteDesktopDefaults>(
|
||||
key: K,
|
||||
value: RemoteDesktopDefaults[K],
|
||||
) => setRdp((current) => ({ ...current, [key]: value }));
|
||||
|
||||
const applySavedTheme = (id: string) => {
|
||||
if (!id) return;
|
||||
@@ -153,10 +117,7 @@ export function ConnectionDefaultsSettings() {
|
||||
async function save() {
|
||||
setSaving(true);
|
||||
try {
|
||||
await Promise.all([
|
||||
defaults.saveTerminalDefaults(terminal),
|
||||
defaults.saveRdpDefaults(rdp),
|
||||
]);
|
||||
await defaults.saveTerminalDefaults(terminal);
|
||||
toast.success(t("newUi.sidebar.connectionDefaults.saved"));
|
||||
setOpen(false);
|
||||
} catch {
|
||||
@@ -204,241 +165,155 @@ export function ConnectionDefaultsSettings() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex border-b border-border">
|
||||
{(
|
||||
[
|
||||
["shell", "newUi.sidebar.connectionDefaults.tabTerminal"],
|
||||
[
|
||||
"desktop",
|
||||
"newUi.sidebar.connectionDefaults.tabRemoteDesktop",
|
||||
],
|
||||
] as const
|
||||
).map(([id, labelKey]) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => setTab(id)}
|
||||
className={`px-3 py-2 text-xs font-medium border-b-2 -mb-px transition-colors ${
|
||||
tab === id
|
||||
? "border-accent-brand text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{t(labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="max-h-[55vh] overflow-y-auto pr-1">
|
||||
{tab === "shell" ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<Field label={t("hosts.fontFamilyLabel")}>
|
||||
<select
|
||||
className={inputClass}
|
||||
value={terminal.fontFamily ?? ""}
|
||||
onChange={(e) =>
|
||||
updateTerminal("fontFamily", e.target.value || undefined)
|
||||
}
|
||||
>
|
||||
<option value="">{inheritLabel}</option>
|
||||
{TERMINAL_FONTS.map((font) => (
|
||||
<option key={font.value} value={font.value}>
|
||||
{font.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
<Field label={t("hosts.colorTheme")}>
|
||||
<select
|
||||
className={inputClass}
|
||||
value={terminal.theme ?? ""}
|
||||
onChange={(e) =>
|
||||
updateTerminal("theme", e.target.value || undefined)
|
||||
}
|
||||
>
|
||||
<option value="">{inheritLabel}</option>
|
||||
{Object.entries(TERMINAL_THEMES)
|
||||
.filter(
|
||||
([key]) =>
|
||||
key !== "termixDark" && key !== "termixLight",
|
||||
)
|
||||
.map(([key, theme]) => (
|
||||
<option key={key} value={key}>
|
||||
{theme.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
{savedThemes.length > 0 && (
|
||||
<Field
|
||||
label={t(
|
||||
"newUi.sidebar.connectionDefaults.savedThemeLabel",
|
||||
)}
|
||||
>
|
||||
<select
|
||||
className={inputClass}
|
||||
value=""
|
||||
onChange={(e) => applySavedTheme(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{t(
|
||||
"newUi.sidebar.connectionDefaults.savedThemePlaceholder",
|
||||
)}
|
||||
</option>
|
||||
{savedThemes.map((theme) => (
|
||||
<option key={theme.id} value={theme.id}>
|
||||
{theme.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label={t("hosts.fontSizeLabel")}>
|
||||
<input
|
||||
className={inputClass}
|
||||
type="number"
|
||||
min={8}
|
||||
max={32}
|
||||
value={terminal.fontSize ?? ""}
|
||||
placeholder={inheritLabel}
|
||||
onChange={(e) =>
|
||||
updateTerminal(
|
||||
"fontSize",
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={t("hosts.lineHeightLabel")}>
|
||||
<input
|
||||
className={inputClass}
|
||||
type="number"
|
||||
min={0.8}
|
||||
max={2}
|
||||
step={0.05}
|
||||
value={terminal.lineHeight ?? ""}
|
||||
placeholder={inheritLabel}
|
||||
onChange={(e) =>
|
||||
updateTerminal(
|
||||
"lineHeight",
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={t("hosts.cursorStyleLabel")}>
|
||||
<select
|
||||
className={inputClass}
|
||||
value={terminal.cursorStyle ?? ""}
|
||||
onChange={(e) =>
|
||||
updateTerminal(
|
||||
"cursorStyle",
|
||||
(e.target.value || undefined) as
|
||||
TerminalDefaults["cursorStyle"] | undefined,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">{inheritLabel}</option>
|
||||
{CURSOR_STYLES.map((style) => (
|
||||
<option key={style.value} value={style.value}>
|
||||
{style.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
<Field label={t("hosts.scrollbackBufferLabel")}>
|
||||
<input
|
||||
className={inputClass}
|
||||
type="number"
|
||||
min={1000}
|
||||
max={100000}
|
||||
step={1000}
|
||||
value={terminal.scrollback ?? ""}
|
||||
placeholder={inheritLabel}
|
||||
onChange={(e) =>
|
||||
updateTerminal(
|
||||
"scrollback",
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<SettingRow
|
||||
label={t("hosts.cursorBlinking")}
|
||||
description={t("hosts.cursorBlinkingDesc")}
|
||||
>
|
||||
<TriStateSelect
|
||||
value={terminal.cursorBlink}
|
||||
onChange={(value) => updateTerminal("cursorBlink", value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<Field label={t("hosts.guac.colorDepth")}>
|
||||
<select
|
||||
className={inputClass}
|
||||
value={rdp.colorDepth ?? ""}
|
||||
onChange={(e) =>
|
||||
updateRdp(
|
||||
"colorDepth",
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">{inheritLabel}</option>
|
||||
<option value="16">16-bit</option>
|
||||
<option value="24">24-bit</option>
|
||||
<option value="32">32-bit</option>
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
<Field label={t("hosts.guac.resizeMethod")}>
|
||||
<select
|
||||
className={inputClass}
|
||||
value={rdp.resizeMethod ?? ""}
|
||||
onChange={(e) =>
|
||||
updateRdp("resizeMethod", e.target.value || undefined)
|
||||
}
|
||||
>
|
||||
<option value="">{inheritLabel}</option>
|
||||
<option value="display-update">
|
||||
{t(
|
||||
"newUi.sidebar.connectionDefaults.resizeDisplayUpdate",
|
||||
)}
|
||||
</option>
|
||||
<option value="reconnect">
|
||||
{t("newUi.sidebar.connectionDefaults.resizeReconnect")}
|
||||
</option>
|
||||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{RDP_TOGGLES.map(([key, labelKey, descriptionKey]) => (
|
||||
<SettingRow
|
||||
key={key}
|
||||
label={t(labelKey)}
|
||||
description={t(descriptionKey)}
|
||||
>
|
||||
<TriStateSelect
|
||||
value={rdp[key] as boolean | undefined}
|
||||
onChange={(value) => updateRdp(key, value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<Field label={t("hosts.fontFamilyLabel")}>
|
||||
<select
|
||||
className={inputClass}
|
||||
value={terminal.fontFamily ?? ""}
|
||||
onChange={(e) =>
|
||||
updateTerminal("fontFamily", e.target.value || undefined)
|
||||
}
|
||||
>
|
||||
<option value="">{inheritLabel}</option>
|
||||
{TERMINAL_FONTS.map((font) => (
|
||||
<option key={font.value} value={font.value}>
|
||||
{font.label}
|
||||
</option>
|
||||
))}
|
||||
</div>
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
<Field label={t("hosts.colorTheme")}>
|
||||
<select
|
||||
className={inputClass}
|
||||
value={terminal.theme ?? ""}
|
||||
onChange={(e) =>
|
||||
updateTerminal("theme", e.target.value || undefined)
|
||||
}
|
||||
>
|
||||
<option value="">{inheritLabel}</option>
|
||||
{Object.entries(TERMINAL_THEMES)
|
||||
.filter(
|
||||
([key]) => key !== "termixDark" && key !== "termixLight",
|
||||
)
|
||||
.map(([key, theme]) => (
|
||||
<option key={key} value={key}>
|
||||
{theme.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
{savedThemes.length > 0 && (
|
||||
<Field
|
||||
label={t("newUi.sidebar.connectionDefaults.savedThemeLabel")}
|
||||
>
|
||||
<select
|
||||
className={inputClass}
|
||||
value=""
|
||||
onChange={(e) => applySavedTheme(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{t(
|
||||
"newUi.sidebar.connectionDefaults.savedThemePlaceholder",
|
||||
)}
|
||||
</option>
|
||||
{savedThemes.map((theme) => (
|
||||
<option key={theme.id} value={theme.id}>
|
||||
{theme.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label={t("hosts.fontSizeLabel")}>
|
||||
<input
|
||||
className={inputClass}
|
||||
type="number"
|
||||
min={8}
|
||||
max={32}
|
||||
value={terminal.fontSize ?? ""}
|
||||
placeholder={inheritLabel}
|
||||
onChange={(e) =>
|
||||
updateTerminal(
|
||||
"fontSize",
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={t("hosts.lineHeightLabel")}>
|
||||
<input
|
||||
className={inputClass}
|
||||
type="number"
|
||||
min={0.8}
|
||||
max={2}
|
||||
step={0.05}
|
||||
value={terminal.lineHeight ?? ""}
|
||||
placeholder={inheritLabel}
|
||||
onChange={(e) =>
|
||||
updateTerminal(
|
||||
"lineHeight",
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={t("hosts.cursorStyleLabel")}>
|
||||
<select
|
||||
className={inputClass}
|
||||
value={terminal.cursorStyle ?? ""}
|
||||
onChange={(e) =>
|
||||
updateTerminal(
|
||||
"cursorStyle",
|
||||
(e.target.value || undefined) as
|
||||
TerminalDefaults["cursorStyle"] | undefined,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">{inheritLabel}</option>
|
||||
{CURSOR_STYLES.map((style) => (
|
||||
<option key={style.value} value={style.value}>
|
||||
{style.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
<Field label={t("hosts.scrollbackBufferLabel")}>
|
||||
<input
|
||||
className={inputClass}
|
||||
type="number"
|
||||
min={1000}
|
||||
max={100000}
|
||||
step={1000}
|
||||
value={terminal.scrollback ?? ""}
|
||||
placeholder={inheritLabel}
|
||||
onChange={(e) =>
|
||||
updateTerminal(
|
||||
"scrollback",
|
||||
e.target.value ? Number(e.target.value) : undefined,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<SettingRow
|
||||
label={t("hosts.cursorBlinking")}
|
||||
description={t("hosts.cursorBlinkingDesc")}
|
||||
>
|
||||
<TriStateSelect
|
||||
value={terminal.cursorBlink}
|
||||
onChange={(value) => updateTerminal("cursorBlink", value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 pt-3 border-t border-border">
|
||||
@@ -447,7 +322,6 @@ export function ConnectionDefaultsSettings() {
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setTerminal({});
|
||||
setRdp({});
|
||||
}}
|
||||
>
|
||||
{t("newUi.sidebar.connectionDefaults.clearAll")}
|
||||
|
||||
@@ -76,6 +76,7 @@ import {
|
||||
} from "./HostEditorData";
|
||||
import { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext";
|
||||
import { HostEditorGeneralTab } from "./HostEditorGeneralTab";
|
||||
import { withProtocolSettings } from "./host-protocols";
|
||||
import { canEditHost } from "./host-permissions";
|
||||
import { VaultProfileManager } from "./VaultProfileManager";
|
||||
import {
|
||||
@@ -331,7 +332,10 @@ export function HostEditor({
|
||||
}
|
||||
// After the host: a new one has no id to scope settings to until it
|
||||
// exists. A failure here must not claim the host itself failed to save.
|
||||
await savePluginHostSettings(Number(saved.id), form.pluginSettings);
|
||||
await savePluginHostSettings(
|
||||
Number(saved.id),
|
||||
withProtocolSettings(form.pluginSettings, protocols),
|
||||
);
|
||||
|
||||
toast.success(host ? t("hosts.hostUpdated") : t("hosts.hostCreated"));
|
||||
setPreviewTerminalTheme(null);
|
||||
|
||||
@@ -2,19 +2,16 @@ import { TERMINAL_THEMES } from "@/lib/terminal-themes";
|
||||
import type { Host } from "@/types/ui-types";
|
||||
import type { SSHHostData } from "@/types";
|
||||
import type { HostDefaults } from "@/api/settings-api";
|
||||
import type {
|
||||
RemoteDesktopDefaults,
|
||||
TerminalDefaults,
|
||||
} from "@/lib/connection-defaults";
|
||||
import type { TerminalDefaults } from "@/lib/connection-defaults";
|
||||
import {
|
||||
listHostProtocols,
|
||||
protocolPort,
|
||||
type HostProtocols,
|
||||
} from "./host-protocols";
|
||||
|
||||
type HostSocks5ProxyNode = NonNullable<Host["socks5ProxyChain"]>[number];
|
||||
|
||||
export type HostProtocols = {
|
||||
enableSsh: boolean;
|
||||
enableRdp: boolean;
|
||||
enableVnc: boolean;
|
||||
enableTelnet: boolean;
|
||||
};
|
||||
export type { HostProtocols };
|
||||
|
||||
export type HostAuthType = Host["authType"];
|
||||
export type HostCursorStyle = NonNullable<
|
||||
@@ -44,23 +41,8 @@ export const terminalAppearanceKeys = [
|
||||
"customThemeColors",
|
||||
] as const satisfies readonly (keyof TerminalDefaults)[];
|
||||
|
||||
export const remoteDesktopDefaultKeys = [
|
||||
"colorDepth",
|
||||
"resizeMethod",
|
||||
"forceLossless",
|
||||
"disableAudio",
|
||||
"enableWallpaper",
|
||||
"enableFontSmoothing",
|
||||
"enableDesktopComposition",
|
||||
"enablePrinting",
|
||||
"enableDrive",
|
||||
"disableCopy",
|
||||
"disablePaste",
|
||||
] as const satisfies readonly (keyof RemoteDesktopDefaults)[];
|
||||
|
||||
export interface UserConnectionDefaults {
|
||||
terminal: TerminalDefaults;
|
||||
rdp: RemoteDesktopDefaults;
|
||||
}
|
||||
|
||||
function hasOwn(value: object | undefined, key: PropertyKey): boolean {
|
||||
@@ -86,18 +68,13 @@ type SnippetResponse = SnippetListItem[] | { snippets?: SnippetListItem[] };
|
||||
/**
|
||||
* Whether the Connection Origin control is meaningful for a host.
|
||||
*
|
||||
* Every protocol Termix can dial from either backend belongs here. RDP, VNC
|
||||
* and Telnet were added once they could originate from the desktop
|
||||
* (Termix-SSH/Support#1240); before that the control was gated on SSH alone,
|
||||
* so a host enabling only those protocols could never reach the setting.
|
||||
* Every protocol Termix can dial from either backend belongs here, plugin
|
||||
* protocols included (Termix-SSH/Support#1240); before that the control was
|
||||
* gated on SSH alone, so a host enabling only remote desktop could never
|
||||
* reach the setting.
|
||||
*/
|
||||
export function connectionOriginAppliesTo(protocols: HostProtocols): boolean {
|
||||
return (
|
||||
protocols.enableSsh ||
|
||||
protocols.enableRdp ||
|
||||
protocols.enableVnc ||
|
||||
protocols.enableTelnet
|
||||
);
|
||||
return Object.values(protocols).some(Boolean);
|
||||
}
|
||||
|
||||
export function mapSnippetResponse(
|
||||
@@ -122,11 +99,6 @@ export function createHostEditorForm(
|
||||
...(connectionDefaults?.terminal ?? {}),
|
||||
...(host?.terminalConfig ?? {}),
|
||||
};
|
||||
const remoteDefaults = host?.enableRdp ? connectionDefaults?.rdp : undefined;
|
||||
const guacamoleConfig = {
|
||||
...(remoteDefaults ?? {}),
|
||||
...(host?.guacamoleConfig ?? {}),
|
||||
};
|
||||
const proxmoxSettings = (host?.pluginSettings?.proxmox ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
@@ -147,9 +119,6 @@ export function createHostEditorForm(
|
||||
ip: host?.ip ?? "",
|
||||
username: host?.username ?? (host ? "" : "root"),
|
||||
sshPort: host?.sshPort ?? host?.port ?? 22,
|
||||
rdpPort: host?.rdpPort ?? 3389,
|
||||
vncPort: host?.vncPort ?? 5900,
|
||||
telnetPort: host?.telnetPort ?? 23,
|
||||
authType: host?.authType ?? "password",
|
||||
useWarpgate: host?.useWarpgate ?? false,
|
||||
shareSshAuth: host?.shareSshAuth ?? false,
|
||||
@@ -223,11 +192,6 @@ export function createHostEditorForm(
|
||||
inheritTerminalAppearance:
|
||||
!host ||
|
||||
terminalAppearanceKeys.every((key) => !hasOwn(host.terminalConfig, key)),
|
||||
inheritRemoteDesktopDefaults:
|
||||
!host ||
|
||||
remoteDesktopDefaultKeys.every(
|
||||
(key) => !hasOwn(host.guacamoleConfig, key),
|
||||
),
|
||||
localEcho: host?.terminalConfig?.localEcho ?? "default",
|
||||
fontSize: terminalConfig.fontSize ?? d?.fontSize ?? 14,
|
||||
fontFamily:
|
||||
@@ -298,8 +262,6 @@ export function createHostEditorForm(
|
||||
? "existing_rdp_password"
|
||||
: (host?.rdpPassword ?? ""),
|
||||
domain: host?.domain ?? "",
|
||||
security: host?.security ?? "",
|
||||
ignoreCert: host?.ignoreCert ?? false,
|
||||
vncCredentialId: host?.vncCredentialId ?? "",
|
||||
vncPassword: host?.hasVncPassword
|
||||
? "existing_vnc_password"
|
||||
@@ -320,7 +282,6 @@ export function createHostEditorForm(
|
||||
telnetAuthType: (host?.telnetAuthType ??
|
||||
(host?.telnetCredentialId ? "credential" : "direct")) as
|
||||
"direct" | "credential",
|
||||
guacamoleConfig,
|
||||
statsConfig: host?.statsConfig ?? {
|
||||
statusCheckEnabled: d?.statusCheckEnabled ?? true,
|
||||
statusCheckInterval: 60,
|
||||
@@ -399,9 +360,11 @@ export function buildHostEditorPayload(
|
||||
const usesPassword = form.authType === "password";
|
||||
const usesAgent = form.authType === "agent";
|
||||
const usesVault = form.authType === "vault";
|
||||
const guacamoleConfig = form.inheritRemoteDesktopDefaults
|
||||
? stripKeys(form.guacamoleConfig, remoteDesktopDefaultKeys)
|
||||
: form.guacamoleConfig;
|
||||
// With SSH off, the host's primary protocol is the first plugin protocol
|
||||
// switched on, and its port stands in for the host port.
|
||||
const primaryProtocol = protocols.enableSsh
|
||||
? undefined
|
||||
: listHostProtocols().find((protocol) => protocols[protocol.settingKey]);
|
||||
const terminalConfig = {
|
||||
theme: form.theme,
|
||||
cursorBlink: form.cursorBlink,
|
||||
@@ -449,22 +412,12 @@ export function buildHostEditorPayload(
|
||||
: terminalConfig;
|
||||
|
||||
return {
|
||||
connectionType: protocols.enableSsh
|
||||
? "ssh"
|
||||
: protocols.enableRdp
|
||||
? "rdp"
|
||||
: protocols.enableVnc
|
||||
? "vnc"
|
||||
: "telnet",
|
||||
connectionType: primaryProtocol?.id ?? "ssh",
|
||||
name: form.name,
|
||||
ip: form.ip,
|
||||
port: protocols.enableSsh
|
||||
? Number(form.sshPort)
|
||||
: protocols.enableRdp
|
||||
? Number(form.rdpPort)
|
||||
: protocols.enableVnc
|
||||
? Number(form.vncPort)
|
||||
: Number(form.telnetPort),
|
||||
port: primaryProtocol
|
||||
? protocolPort(form.pluginSettings, primaryProtocol)
|
||||
: Number(form.sshPort),
|
||||
username: form.username,
|
||||
folder: form.folder,
|
||||
parentHostId: form.parentHostId ? Number(form.parentHostId) : null,
|
||||
@@ -531,13 +484,7 @@ export function buildHostEditorPayload(
|
||||
form.socks5ProxyMode === "chain" ? form.socks5ProxyChain : null,
|
||||
connectionOrigin: form.connectionOrigin,
|
||||
enableSsh: protocols.enableSsh,
|
||||
enableRdp: protocols.enableRdp,
|
||||
enableVnc: protocols.enableVnc,
|
||||
enableTelnet: protocols.enableTelnet,
|
||||
sshPort: Number(form.sshPort),
|
||||
rdpPort: Number(form.rdpPort),
|
||||
vncPort: Number(form.vncPort),
|
||||
telnetPort: Number(form.telnetPort),
|
||||
forceKeyboardInteractive: form.forceKeyboardInteractive,
|
||||
rdpAuthType: protocols.enableRdp ? form.rdpAuthType : null,
|
||||
rdpCredentialId:
|
||||
@@ -557,8 +504,6 @@ export function buildHostEditorPayload(
|
||||
? form.rdpPassword || null
|
||||
: null,
|
||||
rdpDomain: form.domain || null,
|
||||
rdpSecurity: form.security || null,
|
||||
rdpIgnoreCert: form.ignoreCert,
|
||||
vncAuthType: protocols.enableVnc ? form.vncAuthType : null,
|
||||
vncCredentialId:
|
||||
protocols.enableVnc &&
|
||||
@@ -602,11 +547,6 @@ export function buildHostEditorPayload(
|
||||
snippetId: Number(a.snippetId),
|
||||
})),
|
||||
statsConfig: form.statsConfig,
|
||||
guacamoleConfig:
|
||||
(protocols.enableRdp || protocols.enableVnc || protocols.enableTelnet) &&
|
||||
Object.keys(guacamoleConfig).length > 0
|
||||
? guacamoleConfig
|
||||
: null,
|
||||
terminalConfig: protocols.enableSsh ? terminalOverrides : null,
|
||||
};
|
||||
}
|
||||
@@ -9,8 +9,6 @@ import type { Host } from "@/types/ui-types";
|
||||
import {
|
||||
Globe,
|
||||
LayoutGrid,
|
||||
Monitor,
|
||||
MousePointerClick,
|
||||
Plus,
|
||||
Tag,
|
||||
Terminal,
|
||||
@@ -23,6 +21,7 @@ import { getSSHFolders, isElectron } from "@/main-axios";
|
||||
import { connectionOriginAppliesTo } from "./HostEditorData";
|
||||
import type { HostEditorForm, HostProtocols } from "./HostEditorData";
|
||||
import { Select2 } from "@/components/select2";
|
||||
import { useHostProtocols } from "./host-protocols";
|
||||
|
||||
type HostEditorSetField = <K extends keyof HostEditorForm>(
|
||||
key: K,
|
||||
@@ -48,6 +47,7 @@ export function HostEditorGeneralTab({
|
||||
simpleMode?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const pluginProtocols = useHostProtocols();
|
||||
|
||||
// Tracks which picker is shown, independent of whether a value is set yet
|
||||
// -- switching to "parent host" mode with nothing picked shouldn't bounce
|
||||
@@ -116,35 +116,22 @@ export function HostEditorGeneralTab({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 py-3">
|
||||
{[
|
||||
{
|
||||
proto: "enableSsh" as const,
|
||||
proto: "enableSsh",
|
||||
label: t("hosts.tabSsh"),
|
||||
desc: t("hosts.secureShell"),
|
||||
icon: <Terminal className="size-4" />,
|
||||
portField: "sshPort" as const,
|
||||
},
|
||||
{
|
||||
proto: "enableRdp" as const,
|
||||
label: t("hosts.tabRdp"),
|
||||
desc: t("hosts.remoteDesktop"),
|
||||
icon: <Monitor className="size-4" />,
|
||||
portField: "rdpPort" as const,
|
||||
},
|
||||
{
|
||||
proto: "enableVnc" as const,
|
||||
label: t("hosts.tabVnc"),
|
||||
desc: t("hosts.virtualNetwork"),
|
||||
icon: <MousePointerClick className="size-4" />,
|
||||
portField: "vncPort" as const,
|
||||
},
|
||||
{
|
||||
proto: "enableTelnet" as const,
|
||||
label: t("hosts.tabTelnet"),
|
||||
desc: t("hosts.unencryptedShell"),
|
||||
icon: <Terminal className="size-4" />,
|
||||
portField: "telnetPort" as const,
|
||||
},
|
||||
...pluginProtocols.map((protocol) => {
|
||||
const Icon = protocol.icon;
|
||||
return {
|
||||
proto: protocol.settingKey,
|
||||
label: t(protocol.titleKey),
|
||||
desc: protocol.descriptionKey ? t(protocol.descriptionKey) : "",
|
||||
icon: <Icon className="size-4" />,
|
||||
};
|
||||
}),
|
||||
].map(({ proto, label, desc, icon }) => {
|
||||
const enabled = protocols[proto];
|
||||
const enabled = !!protocols[proto];
|
||||
return (
|
||||
<div
|
||||
key={proto}
|
||||
@@ -245,15 +232,12 @@ export function HostEditorGeneralTab({
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
{!protocols.enableSsh &&
|
||||
!protocols.enableRdp &&
|
||||
!protocols.enableVnc &&
|
||||
!protocols.enableTelnet && (
|
||||
<div className="flex items-center gap-3 p-3 border border-border bg-muted/20 text-xs text-muted-foreground">
|
||||
<Globe className="size-4 shrink-0 text-muted-foreground/40" />
|
||||
<span>{t("hosts.enableAtLeastOneProtocol")}</span>
|
||||
</div>
|
||||
)}
|
||||
{!Object.values(protocols).some(Boolean) && (
|
||||
<div className="flex items-center gap-3 p-3 border border-border bg-muted/20 text-xs text-muted-foreground">
|
||||
<Globe className="size-4 shrink-0 text-muted-foreground/40" />
|
||||
<span>{t("hosts.enableAtLeastOneProtocol")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SectionCard
|
||||
title={t("hosts.folderAndAdvanced")}
|
||||
@@ -769,13 +753,7 @@ export function HostEditorGeneralTab({
|
||||
{isElectron() && connectionOriginAppliesTo(protocols) && (
|
||||
<SettingRow
|
||||
label={t("hosts.connectionOrigin")}
|
||||
description={
|
||||
protocols.enableRdp ||
|
||||
protocols.enableVnc ||
|
||||
protocols.enableTelnet
|
||||
? `${t("hosts.connectionOriginDesc")} ${t("hosts.connectionOriginRemoteDesktopNote")}`
|
||||
: t("hosts.connectionOriginDesc")
|
||||
}
|
||||
description={t("hosts.connectionOriginDesc")}
|
||||
>
|
||||
<select
|
||||
className="flex h-7 border border-border bg-background px-2 py-0 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
enabledHostProtocols,
|
||||
hostProtocolFlags,
|
||||
type HostProtocols,
|
||||
} from "./host-protocols";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
@@ -102,12 +107,9 @@ export function HostManager({
|
||||
} | null>(null);
|
||||
const [hostEditorDirty, setHostEditorDirty] = useState(false);
|
||||
const [showUnsavedHostDialog, setShowUnsavedHostDialog] = useState(false);
|
||||
const [editingProtocols, setEditingProtocols] = useState({
|
||||
enableSsh: true,
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
});
|
||||
const [editingProtocols, setEditingProtocols] = useState<HostProtocols>(() =>
|
||||
hostProtocolFlags(null),
|
||||
);
|
||||
const simpleEditor = useUiPreference("hostEditor", "mode") === "simple";
|
||||
// Expanding advanced is a per-session choice; fixing one host's SSH options
|
||||
// shouldn't quietly move the whole app off the Simple preset.
|
||||
@@ -142,12 +144,7 @@ export function HostManager({
|
||||
setEditingHost(host);
|
||||
setEditingCredential(null);
|
||||
setActiveHostTab("general");
|
||||
setEditingProtocols({
|
||||
enableSsh: host.enableSsh,
|
||||
enableRdp: host.enableRdp,
|
||||
enableVnc: host.enableVnc,
|
||||
enableTelnet: host.enableTelnet,
|
||||
});
|
||||
setEditingProtocols(hostProtocolFlags(host));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -204,12 +201,7 @@ export function HostManager({
|
||||
if (action === "add-host") {
|
||||
setEditingHost("new");
|
||||
setEditingCredential(null);
|
||||
setEditingProtocols({
|
||||
enableSsh: true,
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
});
|
||||
setEditingProtocols(hostProtocolFlags(null));
|
||||
setActiveHostTab("general");
|
||||
} else if (action === "add-credential") {
|
||||
setEditingCredential("new");
|
||||
@@ -224,12 +216,7 @@ export function HostManager({
|
||||
const handleAddHost = () => {
|
||||
setEditingHost("new");
|
||||
setEditingCredential(null);
|
||||
setEditingProtocols({
|
||||
enableSsh: true,
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
});
|
||||
setEditingProtocols(hostProtocolFlags(null));
|
||||
setActiveHostTab("general");
|
||||
};
|
||||
const handleAddCredential = () => {
|
||||
@@ -244,12 +231,7 @@ export function HostManager({
|
||||
setEditingHost(host);
|
||||
setEditingCredential(null);
|
||||
setActiveHostTab("general");
|
||||
setEditingProtocols({
|
||||
enableSsh: host.enableSsh,
|
||||
enableRdp: host.enableRdp,
|
||||
enableVnc: host.enableVnc,
|
||||
enableTelnet: host.enableTelnet,
|
||||
});
|
||||
setEditingProtocols(hostProtocolFlags(host));
|
||||
}
|
||||
};
|
||||
window.addEventListener("host-manager:add-host", handleAddHost);
|
||||
@@ -848,9 +830,7 @@ export function HostManager({
|
||||
<option value="">{t("credentials.selectHostOption")}</option>
|
||||
{allHosts
|
||||
.filter(
|
||||
(h) =>
|
||||
h.enableSsh ||
|
||||
(!h.enableRdp && !h.enableVnc && !h.enableTelnet),
|
||||
(h) => h.enableSsh || enabledHostProtocols(h).length === 0,
|
||||
)
|
||||
.map((h) => (
|
||||
<option key={h.id} value={h.id}>
|
||||
|
||||
@@ -109,16 +109,9 @@ export function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
proxmoxStatsConfig:
|
||||
(proxmoxSettings.proxmoxStatsConfig as Host["proxmoxStatsConfig"]) ??
|
||||
null,
|
||||
enableRdp: h.enableRdp != null ? h.enableRdp : h.connectionType === "rdp",
|
||||
enableVnc: h.enableVnc != null ? h.enableVnc : h.connectionType === "vnc",
|
||||
enableTelnet:
|
||||
h.enableTelnet != null ? h.enableTelnet : h.connectionType === "telnet",
|
||||
sshPort:
|
||||
h.sshPort ??
|
||||
(h.connectionType === "ssh" || !h.connectionType ? h.port : 22),
|
||||
rdpPort: h.rdpPort ?? (h.connectionType === "rdp" ? h.port : 3389),
|
||||
vncPort: h.vncPort ?? (h.connectionType === "vnc" ? h.port : 5900),
|
||||
telnetPort: h.telnetPort ?? (h.connectionType === "telnet" ? h.port : 23),
|
||||
rdpAuthType:
|
||||
(h.rdpAuthType as "direct" | "credential") ??
|
||||
(h.rdpCredentialId ? "credential" : "direct"),
|
||||
@@ -128,8 +121,6 @@ export function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
rdpPassword: h.rdpPassword ?? "",
|
||||
hasRdpPassword: !!host.hasRdpPassword || !!h.rdpPassword,
|
||||
domain: h.rdpDomain,
|
||||
security: h.rdpSecurity,
|
||||
ignoreCert: h.rdpIgnoreCert ?? false,
|
||||
vncAuthType:
|
||||
(h.vncAuthType as "direct" | "credential") ??
|
||||
(h.vncCredentialId ? "credential" : "direct"),
|
||||
@@ -160,7 +151,6 @@ export function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
hasSudoPassword:
|
||||
!!host.hasSudoPassword || !!parsedTerminalConfig?.sudoPassword,
|
||||
statsConfig: parseJson(h.statsConfig) as Host["statsConfig"],
|
||||
guacamoleConfig: parseJson(h.guacamoleConfig),
|
||||
forceKeyboardInteractive: h.forceKeyboardInteractive ?? false,
|
||||
useSocks5: h.useSocks5,
|
||||
socks5Host: h.socks5Host,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getErrorMessage } from "../lib/error-message.js";
|
||||
import { enabledHostProtocols, useHostProtocols } from "./host-protocols";
|
||||
import { useSshAuthProviders } from "@/hooks/useSshAuthProviders";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { HostData } from "@/types/index";
|
||||
@@ -87,9 +88,9 @@ function hostGroupNames(host: Host, key: GroupKey): string[] {
|
||||
case "protocol": {
|
||||
const protos: string[] = [];
|
||||
if (host.enableSsh) protos.push("ssh");
|
||||
if (host.enableRdp) protos.push("rdp");
|
||||
if (host.enableVnc) protos.push("vnc");
|
||||
if (host.enableTelnet) protos.push("telnet");
|
||||
for (const protocol of enabledHostProtocols(host)) {
|
||||
protos.push(protocol.id);
|
||||
}
|
||||
return protos.length > 0 ? protos : ["__none__"];
|
||||
}
|
||||
case "auth":
|
||||
@@ -144,9 +145,9 @@ function hostPassesFilters(host: Host, filters: FilterState): boolean {
|
||||
if (filters.protocol.length > 0) {
|
||||
const ok =
|
||||
(filters.protocol.includes("ssh") && host.enableSsh) ||
|
||||
(filters.protocol.includes("rdp") && host.enableRdp) ||
|
||||
(filters.protocol.includes("vnc") && host.enableVnc) ||
|
||||
(filters.protocol.includes("telnet") && host.enableTelnet);
|
||||
enabledHostProtocols(host).some((protocol) =>
|
||||
filters.protocol.includes(protocol.id),
|
||||
);
|
||||
if (!ok) return false;
|
||||
}
|
||||
if (filters.features.length > 0) {
|
||||
@@ -203,6 +204,7 @@ export function HostsPanel({
|
||||
active?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const hostProtocols = useHostProtocols();
|
||||
const sshAuthProviders = useSshAuthProviders();
|
||||
const [hostSearch, setHostSearch] = useState("");
|
||||
const [managerEditing, setManagerEditing] = useState(false);
|
||||
@@ -349,9 +351,6 @@ export function HostsPanel({
|
||||
pin: true,
|
||||
notes: "Main production web server running Nginx",
|
||||
enableSsh: true,
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
sshPort: 22,
|
||||
enableTerminal: true,
|
||||
enableTunnel: false,
|
||||
@@ -370,9 +369,6 @@ export function HostsPanel({
|
||||
folder: "Production",
|
||||
tags: ["database", "production", "postgresql"],
|
||||
enableSsh: true,
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
sshPort: 22,
|
||||
enableTerminal: true,
|
||||
enableTunnel: true,
|
||||
@@ -449,10 +445,6 @@ export function HostsPanel({
|
||||
...h,
|
||||
port: h.port ?? h.sshPort ?? 22,
|
||||
enableSsh: h.enableSsh ?? h.connectionType === "ssh",
|
||||
enableRdp: h.enableRdp ?? h.connectionType === "rdp",
|
||||
enableVnc: h.enableVnc ?? h.connectionType === "vnc",
|
||||
enableTelnet:
|
||||
h.enableTelnet ?? h.connectionType === "telnet",
|
||||
}),
|
||||
);
|
||||
const result = await bulkImportSSHHosts(
|
||||
@@ -768,23 +760,20 @@ export function HostsPanel({
|
||||
<DropdownMenuLabel>
|
||||
{t("hosts.filterProtocolGroup")}
|
||||
</DropdownMenuLabel>
|
||||
{(
|
||||
[
|
||||
["ssh", "Ssh"],
|
||||
["rdp", "Rdp"],
|
||||
["vnc", "Vnc"],
|
||||
["telnet", "Telnet"],
|
||||
] as const
|
||||
).map(([val, key]) => (
|
||||
{[
|
||||
{ id: "ssh", label: t("hosts.filterProtocolSsh") },
|
||||
...hostProtocols.map((protocol) => ({
|
||||
id: protocol.id,
|
||||
label: t(protocol.titleKey),
|
||||
})),
|
||||
].map(({ id, label }) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={val}
|
||||
checked={filterState.protocol.includes(val)}
|
||||
onCheckedChange={() =>
|
||||
handleFilterToggle("protocol", val)
|
||||
}
|
||||
key={id}
|
||||
checked={filterState.protocol.includes(id)}
|
||||
onCheckedChange={() => handleFilterToggle("protocol", id)}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{t(`hosts.filterProtocol${key}`)}
|
||||
{label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
FolderSearch,
|
||||
Monitor,
|
||||
MousePointerClick,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import { Eye, EyeOff, FolderSearch, Terminal } from "lucide-react";
|
||||
import { Input } from "@/components/input";
|
||||
import type { Host } from "@/types/ui-types";
|
||||
import { getCredentials } from "@/api/credentials-api";
|
||||
import { mapCredentials } from "./HostManagerData";
|
||||
import {
|
||||
createQuickConnectHost,
|
||||
type QuickConnectProtocol,
|
||||
} from "./quick-connect-host";
|
||||
|
||||
const DEFAULT_PORTS: Record<QuickConnectProtocol, string> = {
|
||||
ssh: "22",
|
||||
rdp: "3389",
|
||||
vnc: "5900",
|
||||
};
|
||||
import { createQuickConnectHost } from "./quick-connect-host";
|
||||
import { useHostProtocols } from "./host-protocols";
|
||||
import { Select2 } from "@/components/select2";
|
||||
import { resolveHostTabType } from "@/lib/host-connection-tabs";
|
||||
|
||||
@@ -32,7 +17,8 @@ interface QuickConnectPanelProps {
|
||||
export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [host, setHost] = useState("");
|
||||
const [protocol, setProtocol] = useState<QuickConnectProtocol>("ssh");
|
||||
// "ssh", or the id of a plugin protocol offered in Quick Connect.
|
||||
const [protocol, setProtocol] = useState("ssh");
|
||||
const [port, setPort] = useState("22");
|
||||
const [domain, setDomain] = useState("");
|
||||
const [username, setUsername] = useState("root");
|
||||
@@ -53,11 +39,17 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const isDesktop = protocol !== "ssh";
|
||||
const pluginProtocols = useHostProtocols().filter(
|
||||
(entry) => entry.quickConnect,
|
||||
);
|
||||
const selected = pluginProtocols.find((entry) => entry.id === protocol);
|
||||
const isDesktop = !!selected;
|
||||
const defaultPort = (id: string) =>
|
||||
String(pluginProtocols.find((entry) => entry.id === id)?.defaultPort ?? 22);
|
||||
|
||||
const switchProtocol = (next: QuickConnectProtocol) => {
|
||||
const switchProtocol = (next: string) => {
|
||||
// Keep a port the user typed; only swap the protocol default.
|
||||
if (port === DEFAULT_PORTS[protocol]) setPort(DEFAULT_PORTS[next]);
|
||||
if (port === defaultPort(protocol)) setPort(defaultPort(next));
|
||||
setProtocol(next);
|
||||
};
|
||||
|
||||
@@ -67,13 +59,13 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
if (!isDesktop && !username) return;
|
||||
const hostConfig = createQuickConnectHost({
|
||||
ip: host,
|
||||
port: parseInt(port) || parseInt(DEFAULT_PORTS[protocol]),
|
||||
port: parseInt(port) || parseInt(defaultPort(protocol)),
|
||||
username,
|
||||
authType: isDesktop ? "password" : authType,
|
||||
password,
|
||||
key: privateKey,
|
||||
credentialId,
|
||||
protocol,
|
||||
protocol: selected,
|
||||
domain: domain || undefined,
|
||||
});
|
||||
const target = type ?? resolveHostTabType(hostConfig);
|
||||
@@ -90,19 +82,21 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
{t("newUi.sidebar.quickConnect.protocolLabel")}
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{(["ssh", "rdp", "vnc"] as const).map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => switchProtocol(type)}
|
||||
className={`flex-1 py-1 text-[10px] font-semibold border transition-colors uppercase ${
|
||||
protocol === type
|
||||
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand"
|
||||
: "border-border text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{type}
|
||||
</button>
|
||||
))}
|
||||
{["ssh", ...pluginProtocols.map((entry) => entry.id)].map(
|
||||
(type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => switchProtocol(type)}
|
||||
className={`flex-1 py-1 text-[10px] font-semibold border transition-colors uppercase ${
|
||||
protocol === type
|
||||
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand"
|
||||
: "border-border text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{type}
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -206,7 +200,7 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isDesktop && protocol === "rdp" && (
|
||||
{selected?.quickConnect?.showDomain && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("newUi.sidebar.quickConnect.domainLabel")}
|
||||
@@ -264,21 +258,15 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1.5 pt-1">
|
||||
{isDesktop ? (
|
||||
{selected ? (
|
||||
<button
|
||||
onClick={() => connect(protocol)}
|
||||
onClick={() => connect()}
|
||||
className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors"
|
||||
>
|
||||
{protocol === "rdp" ? (
|
||||
<Monitor className="size-3.5" />
|
||||
) : (
|
||||
<MousePointerClick className="size-3.5" />
|
||||
)}
|
||||
{t(
|
||||
protocol === "rdp"
|
||||
? "newUi.sidebar.quickConnect.connectToRdp"
|
||||
: "newUi.sidebar.quickConnect.connectToVnc",
|
||||
)}
|
||||
<selected.icon className="size-3.5" />
|
||||
{t("newUi.sidebar.quickConnect.connectWith", {
|
||||
protocol: t(selected.titleKey),
|
||||
})}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Host, SharePermissionLevel } from "@/types/ui-types";
|
||||
import {
|
||||
AUTH_PROTOCOL_METADATA,
|
||||
isSupportedAuthOverrideProtocol,
|
||||
type AuthOverrideProtocol,
|
||||
} from "@/types/auth-protocols";
|
||||
import { listHostProtocols, protocolEnabled } from "./host-protocols";
|
||||
|
||||
const LEVEL_RANK: Record<SharePermissionLevel, number> = {
|
||||
connect: 1,
|
||||
@@ -36,10 +36,11 @@ export function canOverrideHostAuth(
|
||||
host: Host,
|
||||
protocol: AuthOverrideProtocol,
|
||||
): boolean {
|
||||
const enableField = AUTH_PROTOCOL_METADATA[protocol].enableField;
|
||||
return (
|
||||
!!host.isShared &&
|
||||
isSupportedAuthOverrideProtocol(protocol) &&
|
||||
!!host[enableField]
|
||||
);
|
||||
if (!host.isShared || !isSupportedAuthOverrideProtocol(protocol)) {
|
||||
return false;
|
||||
}
|
||||
if (protocol === "ssh") return !!host.enableSsh;
|
||||
// Other protocols are switched on in their plugin's host settings.
|
||||
const plugin = listHostProtocols().find((entry) => entry.id === protocol);
|
||||
return !!plugin && protocolEnabled(host.pluginSettings, plugin);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { ComponentType } from "react";
|
||||
import { byOrderThenId, createRegistry } from "@/lib/registry";
|
||||
|
||||
type Icon = ComponentType<{ className?: string; size?: number | string }>;
|
||||
|
||||
/**
|
||||
* A connection protocol a plugin adds next to SSH (RDP, VNC). Its on/off
|
||||
* switch and port are the plugin's own host settings, so core reads them
|
||||
* from host.pluginSettings[pluginId] and never knows the protocol by name.
|
||||
*/
|
||||
export interface HostProtocolDef {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
/** The plugin's boolean host setting that switches the protocol on. */
|
||||
settingKey: string;
|
||||
/** The plugin's number host setting holding the port, if it has one. */
|
||||
portKey?: string;
|
||||
defaultPort: number;
|
||||
titleKey: string;
|
||||
descriptionKey?: string;
|
||||
icon: Icon;
|
||||
order?: number;
|
||||
/** Offer it in Quick Connect, optionally with a domain field. */
|
||||
quickConnect?: { showDomain?: boolean };
|
||||
}
|
||||
|
||||
const protocols = createRegistry<HostProtocolDef>(byOrderThenId);
|
||||
|
||||
export const registerHostProtocol = protocols.register;
|
||||
export const useHostProtocols = protocols.useList;
|
||||
export const listHostProtocols = protocols.list;
|
||||
|
||||
/** Which protocols are on, keyed by setting key, with SSH as enableSsh. */
|
||||
export type HostProtocols = { enableSsh: boolean } & Record<string, boolean>;
|
||||
|
||||
type PluginSettingsBag = Record<string, Record<string, unknown>> | undefined;
|
||||
|
||||
function settingsOf(
|
||||
pluginSettings: PluginSettingsBag,
|
||||
protocol: HostProtocolDef,
|
||||
): Record<string, unknown> {
|
||||
return pluginSettings?.[protocol.pluginId] ?? {};
|
||||
}
|
||||
|
||||
export function protocolEnabled(
|
||||
pluginSettings: PluginSettingsBag,
|
||||
protocol: HostProtocolDef,
|
||||
): boolean {
|
||||
return settingsOf(pluginSettings, protocol)[protocol.settingKey] === true;
|
||||
}
|
||||
|
||||
export function protocolPort(
|
||||
pluginSettings: PluginSettingsBag,
|
||||
protocol: HostProtocolDef,
|
||||
): number {
|
||||
const raw = protocol.portKey
|
||||
? Number(settingsOf(pluginSettings, protocol)[protocol.portKey])
|
||||
: NaN;
|
||||
return Number.isInteger(raw) && raw > 0 ? raw : protocol.defaultPort;
|
||||
}
|
||||
|
||||
export function hostProtocolFlags(
|
||||
host: { enableSsh?: boolean; pluginSettings?: PluginSettingsBag } | null,
|
||||
list: HostProtocolDef[] = listHostProtocols(),
|
||||
): HostProtocols {
|
||||
const flags: HostProtocols = {
|
||||
enableSsh: host ? host.enableSsh !== false : true,
|
||||
};
|
||||
for (const protocol of list) {
|
||||
flags[protocol.settingKey] = host
|
||||
? protocolEnabled(host.pluginSettings, protocol)
|
||||
: false;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
/** The protocols a host has switched on, in registration order. */
|
||||
export function enabledHostProtocols(
|
||||
host: { pluginSettings?: PluginSettingsBag },
|
||||
list: HostProtocolDef[] = listHostProtocols(),
|
||||
): HostProtocolDef[] {
|
||||
return list.filter((protocol) =>
|
||||
protocolEnabled(host.pluginSettings, protocol),
|
||||
);
|
||||
}
|
||||
|
||||
/** Writes the editor's protocol switches into each owner's host settings. */
|
||||
export function withProtocolSettings(
|
||||
pluginSettings: Record<string, Record<string, unknown>>,
|
||||
flags: HostProtocols,
|
||||
list: HostProtocolDef[] = listHostProtocols(),
|
||||
): Record<string, Record<string, unknown>> {
|
||||
const next = { ...pluginSettings };
|
||||
for (const protocol of list) {
|
||||
if (!(protocol.settingKey in flags)) continue;
|
||||
next[protocol.pluginId] = {
|
||||
...(next[protocol.pluginId] ?? {}),
|
||||
[protocol.settingKey]: flags[protocol.settingKey] === true,
|
||||
};
|
||||
}
|
||||
return next;
|
||||
}
|
||||
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user